diff --git a/webapp/components/FilterMenu/FilterMenu.spec.js b/webapp/components/FilterMenu/FilterMenu.spec.js index 948abb0eb..c377e1838 100644 --- a/webapp/components/FilterMenu/FilterMenu.spec.js +++ b/webapp/components/FilterMenu/FilterMenu.spec.js @@ -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) }) diff --git a/webapp/components/FilterMenu/FilterMenu.vue b/webapp/components/FilterMenu/FilterMenu.vue index 269c86250..c306588c7 100644 --- a/webapp/components/FilterMenu/FilterMenu.vue +++ b/webapp/components/FilterMenu/FilterMenu.vue @@ -28,7 +28,7 @@ diff --git a/webapp/components/FilterPosts/FilterPostsMenuItems.vue b/webapp/components/FilterPosts/FilterPostsMenuItems.vue index ab7e5d491..4db44aa41 100644 --- a/webapp/components/FilterPosts/FilterPostsMenuItems.vue +++ b/webapp/components/FilterPosts/FilterPostsMenuItems.vue @@ -15,8 +15,8 @@ @@ -40,7 +40,7 @@ @@ -78,7 +78,7 @@ name="filter-by-followed-authors-only" icon="user-plus" :primary="filteredByUsersFollowed" - @click="toggleOnlyFollowed" + @click="toggleFilteredByFollowed(user.id)" /> @@ -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) - }, }, } diff --git a/webapp/layouts/default.vue b/webapp/layouts/default.vue index 28b198a79..27aab058a 100644 --- a/webapp/layouts/default.vue +++ b/webapp/layouts/default.vue @@ -183,7 +183,6 @@ export default { isAdmin: 'auth/isAdmin', quickSearchResults: 'search/quickResults', quickSearchPending: 'search/quickPending', - showFilterPostsDropdown: 'default/showFilterPostsDropdown', usersFollowedFilter: 'posts/usersFollowedFilter', categoriesFilter: 'posts/categoriesFilter', }), @@ -223,6 +222,10 @@ export default { } return routes }, + showFilterPostsDropdown() { + const [firstRoute] = this.$route.matched + return firstRoute.name === 'index' + }, }, watch: { Category(category) { diff --git a/webapp/pages/index.spec.js b/webapp/pages/index.spec.js index 920757d01..805640ef6 100644 --- a/webapp/pages/index.spec.js +++ b/webapp/pages/index.spec.js @@ -5,7 +5,6 @@ import Styleguide from '@human-connection/styleguide' import Filters from '~/plugins/vue-filters' import VTooltip from 'v-tooltip' import FilterMenu from '~/components/FilterMenu/FilterMenu' -import { mutations } from '~/store/default' const localVue = createLocalVue() @@ -27,25 +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': () => {}, - }, - mutations: { - 'default/SET_SHOW_FILTER_POSTS_DROPDOWN': mutations.SET_SHOW_FILTER_POSTS_DROPDOWN, }, }) mocks = { @@ -106,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, { @@ -129,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) }) }) }) diff --git a/webapp/pages/index.vue b/webapp/pages/index.vue index e917433c8..683e5eb65 100644 --- a/webapp/pages/index.vue +++ b/webapp/pages/index.vue @@ -16,7 +16,7 @@ - + @@ -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,11 @@ export default { data() { const { hashtag = null } = this.$route.query return { + posts: [], + hasMore: true, // Initialize your apollo data - page: 1, + offset: 0, pageSize: 12, - filter: {}, hashtag, placeholder: this.$t('sorting.newest'), selected: this.$t('sorting.newest'), @@ -91,67 +92,37 @@ export default { ], } }, - mounted() { - this.toggleShowFilterPostsDropdown(true) - if (this.hashtag) { - this.changeFilterBubble({ tags_some: { name: this.hashtag } }) - } - }, - beforeDestroy() { - this.toggleShowFilterPostsDropdown(false) - }, - 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', - toggleShowFilterPostsDropdown: 'default/SET_SHOW_FILTER_POSTS_DROPDOWN', - }), - 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({ @@ -160,31 +131,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: { @@ -193,16 +145,21 @@ 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. + this.hasMore = Post.length >= this.pageSize + const posts = uniqBy([...this.posts, ...Post], 'id') + this.posts = posts }, fetchPolicy: 'cache-and-network', }, diff --git a/webapp/store/default.js b/webapp/store/default.js deleted file mode 100644 index 7af564910..000000000 --- a/webapp/store/default.js +++ /dev/null @@ -1,17 +0,0 @@ -export const state = () => { - return { - showFilterPostsDropdown: false, - } -} - -export const getters = { - showFilterPostsDropdown(state) { - return state.showFilterPostsDropdown || false - }, -} - -export const mutations = { - SET_SHOW_FILTER_POSTS_DROPDOWN(state, boolean) { - state.showFilterPostsDropdown = boolean || null - }, -} diff --git a/webapp/store/postsFilter.js b/webapp/store/postsFilter.js new file mode 100644 index 000000000..682c658c1 --- /dev/null +++ b/webapp/store/postsFilter.js @@ -0,0 +1,50 @@ +import get from 'lodash/get' +import update from 'lodash/update' +import xor from 'lodash/xor' +import isEmpty from 'lodash/isEmpty' +import clone from 'lodash/clone' + +export const state = () => { + return { + filter: {}, + } +} + +export const mutations = { + TOGGLE_FILTER_BY_FOLLOWED(state, currentUserId) { + const filter = clone(state.filter) + const id = get(filter, 'author.followedBy_some.id') + if (id) { + delete filter.author + state.filter = filter + } else { + state.filter = { + ...filter, + author: { followedBy_some: { id: currentUserId } }, + } + } + }, + RESET_CATEGORIES(state) { + const filter = clone(state.filter) + delete filter.categories_some + state.filter = filter + }, + TOGGLE_CATEGORY(state, categoryId) { + const filter = clone(state.filter) + update(filter, 'categories_some.id_in', categoryIds => xor(categoryIds, [categoryId])) + if (isEmpty(get(filter, 'categories_some.id_in'))) delete filter.categories_some + state.filter = filter + }, +} + +export const getters = { + postsFilter(state) { + return state.filter + }, + filteredCategoryIds(state) { + return get(state.filter, 'categories_some.id_in') || [] + }, + filteredByUsersFollowed(state) { + return !!get(state.filter, 'author.followedBy_some.id') + }, +} diff --git a/webapp/store/postsFilter.spec.js b/webapp/store/postsFilter.spec.js new file mode 100644 index 000000000..46b5f075c --- /dev/null +++ b/webapp/store/postsFilter.spec.js @@ -0,0 +1,126 @@ +import { getters, mutations } from './postsFilter.js' + +let state +let testAction + +describe('getters', () => { + describe('filteredCategoryIds', () => { + it('returns category ids if filter is set', () => { + state = { filter: { categories_some: { id_in: [24] } } } + expect(getters.filteredCategoryIds(state)).toEqual([24]) + }) + + it('returns empty array if filter is not set', () => { + state = { filter: { author: { followedBy_some: { id: 7 } } } } + expect(getters.filteredCategoryIds(state)).toEqual([]) + }) + }) + + describe('postsFilter', () => { + it('returns filter', () => { + state = { filter: { author: { followedBy_some: { id: 7 } } } } + expect(getters.postsFilter(state)).toEqual({ author: { followedBy_some: { id: 7 } } }) + }) + }) + + describe('filteredByUsersFollowed', () => { + it('returns true if filter is set', () => { + state = { filter: { author: { followedBy_some: { id: 7 } } } } + expect(getters.filteredByUsersFollowed(state)).toBe(true) + }) + + it('returns false if filter is not set', () => { + state = { filter: { categories_some: { id_in: [23] } } } + expect(getters.filteredByUsersFollowed(state)).toBe(false) + }) + }) +}) + +describe('mutations', () => { + describe('RESET_CATEGORIES', () => { + beforeEach(() => { + testAction = categoryId => { + mutations.RESET_CATEGORIES(state, categoryId) + return getters.postsFilter(state) + } + }) + it('resets the categories filter', () => { + state = { + filter: { + author: { followedBy_some: { id: 7 } }, + categories_some: { id_in: [23] }, + }, + } + expect(testAction(23)).toEqual({ author: { followedBy_some: { id: 7 } } }) + }) + }) + + describe('TOGGLE_CATEGORY', () => { + beforeEach(() => { + testAction = categoryId => { + mutations.TOGGLE_CATEGORY(state, categoryId) + return getters.postsFilter(state) + } + }) + + it('creates category filter if empty', () => { + state = { filter: {} } + expect(testAction(23)).toEqual({ categories_some: { id_in: [23] } }) + }) + + it('adds category id not present', () => { + state = { filter: { categories_some: { id_in: [24] } } } + expect(testAction(23)).toEqual({ categories_some: { id_in: [24, 23] } }) + }) + + it('removes category id if present', () => { + state = { filter: { categories_some: { id_in: [23, 24] } } } + const result = testAction(23) + expect(result).toEqual({ categories_some: { id_in: [24] } }) + }) + + it('removes category filter if empty', () => { + state = { filter: { categories_some: { id_in: [23] } } } + expect(testAction(23)).toEqual({}) + }) + + it('does not get in the way of other filters', () => { + state = { + filter: { + author: { followedBy_some: { id: 7 } }, + categories_some: { id_in: [23] }, + }, + } + expect(testAction(23)).toEqual({ author: { followedBy_some: { id: 7 } } }) + }) + }) + + describe('TOGGLE_FILTER_BY_FOLLOWED', () => { + beforeEach(() => { + testAction = userId => { + mutations.TOGGLE_FILTER_BY_FOLLOWED(state, userId) + return getters.postsFilter(state) + } + }) + + describe('given empty filter', () => { + beforeEach(() => { + state = { filter: {} } + }) + + it('attaches the id of the current user to the filter object', () => { + expect(testAction(4711)).toEqual({ author: { followedBy_some: { id: 4711 } } }) + }) + }) + + describe('already filtered', () => { + beforeEach(() => { + state = { filter: { author: { followedBy_some: { id: 4711 } } } } + }) + + it('remove the id of the current user from the filter object', () => { + expect(testAction(4711)).toEqual({}) + }) + }) + }) +})