Merge pull request #1169 from Human-Connection/964-filter-posts-by-followers

Move filter by followers functionality to filter dropdown
This commit is contained in:
Robert Schäfer 2019-08-09 11:57:17 +02:00 committed by GitHub
commit 3261237b48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 306 additions and 125 deletions

View File

@ -13,57 +13,39 @@ describe('FilterMenu.vue', () => {
let mocks
let propsData
const createWrapper = mountMethod => {
return mountMethod(FilterMenu, {
propsData,
mocks,
localVue,
})
}
beforeEach(() => {
mocks = { $t: () => {} }
propsData = {}
})
describe('given a user', () => {
beforeEach(() => {
propsData = {
user: {
id: '4711',
},
hashtag: {},
}
})
describe('mount', () => {
const Wrapper = () => {
return mount(FilterMenu, { mocks, localVue, propsData })
}
beforeEach(() => {
wrapper = createWrapper(mount)
wrapper = Wrapper()
})
it('renders a card', () => {
it('does not render a card if there are no hashtags', () => {
expect(wrapper.is('.ds-card')).toBe(true)
})
describe('click "filter-by-followed-authors-only" button', () => {
it('emits filterBubble object', () => {
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
expect(wrapper.emitted('changeFilterBubble')).toBeTruthy()
})
it('renders a card if there are hashtags', () => {
propsData.hashtag = { hashtag: 'Frieden' }
wrapper = Wrapper()
expect(wrapper.is('.ds-card')).toBe(true)
})
it('toggles filterBubble.author property', () => {
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
expect(wrapper.emitted('changeFilterBubble')[0]).toEqual([
{ author: { followedBy_some: { id: '4711' } } },
])
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
expect(wrapper.emitted('changeFilterBubble')[1]).toEqual([{}])
})
it('makes button primary', () => {
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
expect(
wrapper.find({ name: 'filter-by-followed-authors-only' }).classes('ds-button-primary'),
).toBe(true)
describe('click "clear-search-button" button', () => {
it('emits clearSearch', () => {
wrapper.find({ name: 'clear-search-button' }).trigger('click')
expect(wrapper.emitted().clearSearch).toHaveLength(1)
})
})
})

View File

@ -1,26 +1,6 @@
<template>
<ds-card class="filter-menu-card">
<ds-flex>
<ds-flex-item class="filter-menu-title">
<ds-heading size="h3">{{ $t('filter-menu.title') }}</ds-heading>
</ds-flex-item>
<ds-flex-item>
<div class="filter-menu-buttons">
<ds-button
v-tooltip="{
content: this.$t('contribution.filterFollow'),
placement: 'left',
delay: { show: 500 },
}"
name="filter-by-followed-authors-only"
icon="user-plus"
:primary="!!filterAuthorIsFollowedById"
@click="toggleOnlyFollowed"
/>
</div>
</ds-flex-item>
</ds-flex>
<div v-if="hashtag">
<ds-card v-show="hashtag" class="filter-menu-card">
<div>
<ds-space margin-bottom="x-small" />
<ds-flex>
<ds-flex-item>
@ -34,7 +14,7 @@
placement: 'left',
delay: { show: 500 },
}"
name="filter-by-followed-authors-only"
name="clear-search-button"
icon="close"
@click="clearSearch"
/>
@ -48,31 +28,9 @@
<script>
export default {
props: {
user: { type: Object, required: true },
hashtag: { type: Object, default: null },
},
data() {
return {
filter: {},
}
},
computed: {
filterAuthorIsFollowedById() {
const { author = {} } = this.filter
/* eslint-disable camelcase */
const { followedBy_some = {} } = author
const { id } = followedBy_some
/* eslint-enable */
return id
},
},
methods: {
toggleOnlyFollowed() {
this.filter = this.filterAuthorIsFollowedById
? {}
: { author: { followedBy_some: { id: this.user.id } } }
this.$emit('changeFilterBubble', this.filter)
},
clearSearch() {
this.$emit('clearSearch')
},

View File

@ -4,8 +4,7 @@ import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex'
import FilterPosts from './FilterPosts.vue'
import FilterPostsMenuItem from './FilterPostsMenuItems.vue'
import { mutations } from '~/store/posts'
import { mutations, getters } from '~/store/posts'
const localVue = createLocalVue()
localVue.use(Styleguide)
@ -53,6 +52,21 @@ describe('FilterPosts.vue', () => {
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 = () => {
@ -60,6 +74,13 @@ describe('FilterPosts.vue', () => {
}
beforeEach(() => {
store.replaceState({
filteredByCategories: false,
filteredByUsersFollowed: false,
usersFollowedFilter: {},
categoriesFilter: {},
selectedCategoryIds: [],
})
wrapper = Wrapper()
menuToggle = wrapper.findAll('a').at(0)
menuToggle.trigger('click')
@ -130,5 +151,42 @@ describe('FilterPosts.vue', () => {
const filterPostsMenuItem = wrapper.find(FilterPostsMenuItem)
expect(filterPostsMenuItem.vm.selectedCategoryIds).toEqual([])
})
describe('click "filter-by-followed-authors-only" button', () => {
beforeEach(() => {
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)
})
})
})
})

View File

@ -5,7 +5,7 @@
<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" />
<filter-posts-menu-items :chunk="chunk" @filterPosts="filterPosts" :user="currentUser" />
</template>
</dropdown>
</template>
@ -13,7 +13,7 @@
import _ from 'lodash'
import Dropdown from '~/components/Dropdown'
import { filterPosts } from '~/graphql/PostQuery.js'
import { mapMutations } from 'vuex'
import { mapGetters, mapMutations } from 'vuex'
import FilterPostsMenuItems from '~/components/FilterPosts/FilterPostsMenuItems'
export default {
@ -32,6 +32,9 @@ export default {
}
},
computed: {
...mapGetters({
currentUser: 'auth/user',
}),
chunk() {
return _.chunk(this.categories, 2)
},
@ -40,13 +43,12 @@ export default {
...mapMutations({
setPosts: 'posts/SET_POSTS',
}),
filterPosts(categoryIds) {
const filter = categoryIds.length ? { categories_some: { id_in: categoryIds } } : {}
filterPosts(filter) {
this.$apollo
.query({
query: filterPosts(this.$i18n),
variables: {
filter: filter,
filter,
first: this.pageSize,
offset: 0,
},

View File

@ -2,7 +2,7 @@
<ds-container>
<ds-space />
<ds-flex id="filter-posts-header">
<ds-heading tag="h4">{{ $t('filter-posts.header') }}</ds-heading>
<ds-heading tag="h4">{{ $t('filter-posts.categories.header') }}</ds-heading>
<ds-space margin-bottom="large" />
</ds-flex>
<ds-flex>
@ -16,10 +16,10 @@
<ds-button
icon="check"
@click.stop.prevent="toggleCategory()"
:primary="allCategories"
:primary="!filteredByCategories"
/>
<ds-flex-item>
<label class="category-labels">{{ $t('filter-posts.all') }}</label>
<label class="category-labels">{{ $t('filter-posts.categories.all') }}</label>
</ds-flex-item>
<ds-space />
</ds-flex-item>
@ -55,41 +55,107 @@
</ds-flex>
</ds-flex-item>
</ds-flex>
<ds-space />
<ds-flex id="filter-posts-by-followers-header">
<ds-heading tag="h4">{{ $t('filter-posts.general.header') }}</ds-heading>
<ds-space margin-bottom="large" />
</ds-flex>
<ds-flex>
<ds-flex-item
:width="{ base: '100%', sm: '100%', md: '100%', lg: '10%' }"
class="categories-menu-item"
>
<ds-flex>
<ds-flex-item width="10%" />
<ds-flex-item width="100%">
<div class="follow-button">
<ds-button
v-tooltip="{
content: this.$t('contribution.filterFollow'),
placement: 'left',
delay: { show: 500 },
}"
name="filter-by-followed-authors-only"
icon="user-plus"
:primary="filteredByUsersFollowed"
@click="toggleOnlyFollowed"
/>
<ds-flex-item>
<label class="follow-label">{{ $t('filter-posts.followers.label') }}</label>
</ds-flex-item>
<ds-space />
</div>
</ds-flex-item>
</ds-flex>
</ds-flex-item>
<ds-space margin-bottom="large" />
</ds-flex>
</ds-container>
</template>
<script>
import { mapGetters, mapMutations } from 'vuex'
export default {
props: {
user: { type: Object, required: true },
chunk: { type: Array, default: () => [] },
},
data() {
return {
selectedCategoryIds: [],
allCategories: true,
filter: {},
}
},
computed: {
...mapGetters({
filteredByUsersFollowed: 'posts/filteredByUsersFollowed',
filteredByCategories: 'posts/filteredByCategories',
usersFollowedFilter: 'posts/usersFollowedFilter',
categoriesFilter: 'posts/categoriesFilter',
selectedCategoryIds: 'posts/selectedCategoryIds',
}),
},
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',
}),
isActive(id) {
const index = this.selectedCategoryIds.indexOf(id)
if (index > -1) {
if (index > -1 && this.setFilteredByCategories) {
return true
}
return false
},
toggleCategory(id) {
if (!id) {
this.selectedCategoryIds = []
this.allCategories = true
toggleCategory(categoryId) {
if (!categoryId) {
this.setSelectedCategoryIds()
} else {
const index = this.selectedCategoryIds.indexOf(id)
if (index > -1) {
this.selectedCategoryIds.splice(index, 1)
} else {
this.selectedCategoryIds.push(id)
}
this.allCategories = false
this.setSelectedCategoryIds(categoryId)
}
this.$emit('filterPosts', this.selectedCategoryIds)
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)
},
},
}
@ -99,6 +165,10 @@ export default {
display: block;
}
#filter-posts-by-followers-header {
display: block;
}
.categories-menu-item {
text-align: center;
}
@ -107,7 +177,8 @@ export default {
justify-content: center;
}
.category-labels {
.category-labels,
.follow-label {
font-size: $font-size-small;
}
@ -122,5 +193,8 @@ export default {
#filter-posts-header {
text-align: center;
}
.follow-button {
float: left;
}
}
</style>

View File

@ -38,7 +38,12 @@
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
>
<no-ssr>
<filter-posts placement="top-start" offset="8" :categories="categories" />
<filter-posts
v-show="showFilterPostsDropdown"
placement="top-start"
offset="8"
:categories="categories"
/>
</no-ssr>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '100%', md: '10%', lg: '2%' }" />
@ -142,7 +147,7 @@
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import { mapGetters, mapActions, mapMutations } from 'vuex'
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import SearchInput from '~/components/SearchInput.vue'
import Modal from '~/components/Modal'
@ -178,6 +183,9 @@ export default {
isAdmin: 'auth/isAdmin',
quickSearchResults: 'search/quickResults',
quickSearchPending: 'search/quickPending',
showFilterPostsDropdown: 'default/showFilterPostsDropdown',
usersFollowedFilter: 'posts/usersFollowedFilter',
categoriesFilter: 'posts/categoriesFilter',
}),
userName() {
const { name } = this.user || {}
@ -227,6 +235,9 @@ export default {
quickSearch: 'search/quickSearch',
fetchPosts: 'posts/fetchPosts',
}),
...mapMutations({
setFilteredByFollowers: 'posts/SET_FILTERED_BY_FOLLOWERS',
}),
goToPost(item) {
this.$nextTick(() => {
this.$router.push({
@ -247,7 +258,14 @@ export default {
},
redirectToRoot() {
this.$router.replace('/')
this.fetchPosts({ i18n: this.$i18n, filter: {} })
this.fetchPosts({
i18n: this.$i18n,
filter: {
...this.usersFollowedFilter,
...this.categoriesFilter,
...this.filter,
},
})
},
},
apollo: {

View File

@ -5,8 +5,16 @@
"clearSearch": "Suche löschen"
},
"filter-posts": {
"header": "Themenkategorien",
"all": "Alle"
"categories": {
"header": "Themenkategorien",
"all": "Alle"
},
"general": {
"header": "Filtern nach..."
},
"followers": {
"label": "Benutzern, denen ich folge"
}
},
"site": {
"made": "Mit &#10084; gemacht",

View File

@ -5,8 +5,16 @@
"clearSearch": "Clear search"
},
"filter-posts": {
"header": "Categories of Content",
"all": "All"
"categories": {
"header": "Categories of Content",
"all": "All"
},
"general": {
"header": "Filter by..."
},
"followers": {
"label": "Users I follow"
}
},
"site": {
"made": "Made with &#10084;",

View File

@ -5,6 +5,7 @@ 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()
@ -40,6 +41,11 @@ describe('PostIndex', () => {
'auth/user': () => {
return { id: 'u23' }
},
'posts/usersFollowedFilter': () => {},
'posts/categoriesFilter': () => {},
},
mutations: {
'default/SET_SHOW_FILTER_POSTS_DROPDOWN': mutations.SET_SHOW_FILTER_POSTS_DROPDOWN,
},
})
mocks = {
@ -95,11 +101,6 @@ describe('PostIndex', () => {
wrapper = Wrapper()
})
it('refetches Posts when changeFilterBubble is emitted', () => {
wrapper.find(FilterMenu).vm.$emit('changeFilterBubble')
expect(mocks.$apollo.queries.Post.refetch).toHaveBeenCalledTimes(1)
})
it('clears the search when the filter menu emits clearSearch', () => {
wrapper.find(FilterMenu).vm.$emit('clearSearch')
expect(wrapper.vm.hashtag).toBeNull()

View File

@ -2,12 +2,7 @@
<div>
<ds-flex :width="{ base: '100%' }" gutter="base">
<ds-flex-item>
<filter-menu
:user="currentUser"
@changeFilterBubble="changeFilterBubble"
:hashtag="hashtag"
@clearSearch="clearSearch"
/>
<filter-menu :hashtag="hashtag" @clearSearch="clearSearch" />
</ds-flex-item>
<ds-flex-item>
<div class="sorting-dropdown">
@ -97,10 +92,14 @@ 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)
@ -110,6 +109,8 @@ export default {
...mapGetters({
currentUser: 'auth/user',
posts: 'posts/posts',
usersFollowedFilter: 'posts/usersFollowedFilter',
categoriesFilter: 'posts/categoriesFilter',
}),
tags() {
return this.posts ? this.posts.tags.map(tag => tag.name) : '-'
@ -121,6 +122,7 @@ export default {
methods: {
...mapMutations({
setPosts: 'posts/SET_POSTS',
toggleShowFilterPostsDropdown: 'default/SET_SHOW_FILTER_POSTS_DROPDOWN',
}),
changeFilterBubble(filter) {
if (this.hashtag) {
@ -133,6 +135,11 @@ export default {
this.$apollo.queries.Post.refetch()
},
toggleOnlySorting(x) {
this.filter = {
...this.usersFollowedFilter,
...this.categoriesFilter,
}
this.sortingIcon = x.icons
this.sorting = x.order
this.$apollo.queries.Post.refetch()
@ -187,7 +194,11 @@ export default {
},
variables() {
return {
filter: this.filter,
filter: {
...this.usersFollowedFilter,
...this.categoriesFilter,
...this.filter,
},
first: this.pageSize,
offset: 0,
orderBy: this.sorting,

17
webapp/store/default.js Normal file
View File

@ -0,0 +1,17 @@
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
},
}

View File

@ -3,6 +3,11 @@ import gql from 'graphql-tag'
export const state = () => {
return {
posts: [],
filteredByUsersFollowed: false,
filteredByCategories: false,
usersFollowedFilter: {},
categoriesFilter: {},
selectedCategoryIds: [],
}
}
@ -10,12 +15,51 @@ export const mutations = {
SET_POSTS(state, posts) {
state.posts = posts || null
},
SET_FILTERED_BY_FOLLOWERS(state, boolean) {
state.filteredByUsersFollowed = boolean || null
},
SET_FILTERED_BY_CATEGORIES(state, boolean) {
state.filteredByCategories = boolean || null
},
SET_USERS_FOLLOWED_FILTER(state, filter) {
state.usersFollowedFilter = filter || null
},
SET_CATEGORIES_FILTER(state, filter) {
state.categoriesFilter = filter || null
},
SET_SELECTED_CATEGORY_IDS(state, categoryId) {
if (!categoryId) {
state.selectedCategoryIds = []
} else {
const index = state.selectedCategoryIds.indexOf(categoryId)
if (index > -1) {
state.selectedCategoryIds.splice(index, 1)
} else {
state.selectedCategoryIds.push(categoryId)
}
}
},
}
export const getters = {
posts(state) {
return state.posts || []
},
filteredByUsersFollowed(state) {
return state.filteredByUsersFollowed || false
},
filteredByCategories(state) {
return state.filteredByCategories || false
},
usersFollowedFilter(state) {
return state.usersFollowedFilter || {}
},
categoriesFilter(state) {
return state.categoriesFilter || {}
},
selectedCategoryIds(state) {
return state.selectedCategoryIds || []
},
}
export const actions = {
@ -24,7 +68,7 @@ export const actions = {
const {
data: { Post },
} = await client.query({
query: gql(`
query: gql`
query Post($filter: _PostFilter, $first: Int, $offset: Int) {
Post(filter: $filter, first: $first, offset: $offset) {
id
@ -63,7 +107,7 @@ export const actions = {
}
shoutedCount
}
}`),
}`,
variables: {
filter,
first: 12,