Merge pull request #1490 from Human-Connection/1488-filter-posts-by-emotion

Filter posts by emotions
This commit is contained in:
Robert Schäfer 2019-09-10 15:53:05 +02:00 committed by GitHub
commit 6cebb61d0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 248 additions and 81 deletions

View File

@ -91,7 +91,7 @@ afterEach(async () => {
})
describe('Post', () => {
const postQuery = gql`
const postQueryFilteredByCategories = gql`
query Post($filter: _PostFilter) {
Post(filter: $filter) {
id
@ -102,13 +102,28 @@ describe('Post', () => {
}
`
const postQueryFilteredByEmotions = gql`
query Post($filter: _PostFilter) {
Post(filter: $filter) {
id
emotions {
emotion
}
}
}
`
describe('can be filtered', () => {
it('by categories', async () => {
await Promise.all([
let post31, post32
beforeEach(async () => {
;[post31, post32] = await Promise.all([
factory.create('Post', { id: 'p31', categoryIds: ['cat4'] }),
factory.create('Post', { id: 'p32', categoryIds: ['cat15'] }),
factory.create('Post', { id: 'p33', categoryIds: ['cat9'] }),
])
})
it('by categories', async () => {
const expected = {
data: {
Post: [
@ -120,7 +135,50 @@ describe('Post', () => {
},
}
variables = { ...variables, filter: { categories_some: { id_in: ['cat9'] } } }
await expect(query({ query: postQuery, variables })).resolves.toMatchObject(expected)
await expect(
query({ query: postQueryFilteredByCategories, variables }),
).resolves.toMatchObject(expected)
})
it('by emotions', async () => {
const expected = {
data: {
Post: [
{
id: 'p31',
emotions: [{ emotion: 'happy' }],
},
],
},
}
await user.relateTo(post31, 'emoted', { emotion: 'happy' })
variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy'] } } }
await expect(query({ query: postQueryFilteredByEmotions, variables })).resolves.toMatchObject(
expected,
)
})
it('supports filtering by multiple emotions', async () => {
const expected = [
{
id: 'p31',
emotions: [{ emotion: 'happy' }],
},
{
id: 'p32',
emotions: [{ emotion: 'cry' }],
},
]
await user.relateTo(post31, 'emoted', { emotion: 'happy' })
await user.relateTo(post32, 'emoted', { emotion: 'cry' })
variables = { ...variables, filter: { emotions_some: { emotion_in: ['happy', 'cry'] } } }
await expect(query({ query: postQueryFilteredByEmotions, variables })).resolves.toMatchObject(
{
data: {
Post: expect.arrayContaining(expected),
},
},
)
})
})
})

View File

@ -3,8 +3,8 @@ type EMOTED @relation(name: "EMOTED") {
to: Post
emotion: Emotion
#createdAt: DateTime
#updatedAt: DateTime
# createdAt: DateTime
# updatedAt: DateTime
createdAt: String
updatedAt: String
}
@ -15,6 +15,12 @@ input _EMOTEDInput {
updatedAt: String
}
input _PostEMOTEDFilter {
emotion_in: [Emotion]
createdAt: String
updatedAt: String
}
type Mutation {
AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
RemovePostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED

View File

@ -30,7 +30,7 @@ export default function create() {
let { categories, categoryIds } = args
delete args.categories
delete args.categoryIds
if (categories && categoryIds) throw new Error('You provided both category and categoryIds')
if (categories && categoryIds) throw new Error('You provided both categories and categoryIds')
if (categoryIds)
categories = await Promise.all(categoryIds.map(id => neodeInstance.find('Category', id)))
categories = categories || (await Promise.all([factoryInstance.create('Category')]))

View File

@ -1,6 +1,5 @@
<template>
<ds-container>
<ds-space />
<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" />
@ -57,65 +56,22 @@
</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="toggleFilteredByFollowed(user.id)"
/>
<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>
</ds-space>
</template>
<script>
import { mapGetters, mapMutations } from 'vuex'
export default {
props: {
user: { type: Object, required: true },
chunk: { type: Array, default: () => [] },
},
data() {
return {
filter: {},
}
},
computed: {
...mapGetters({
filteredCategoryIds: 'postsFilter/filteredCategoryIds',
filteredByUsersFollowed: 'postsFilter/filteredByUsersFollowed',
}),
},
methods: {
...mapMutations({
toggleFilteredByFollowed: 'postsFilter/TOGGLE_FILTER_BY_FOLLOWED',
resetCategories: 'postsFilter/RESET_CATEGORIES',
toggleCategory: 'postsFilter/TOGGLE_CATEGORY',
}),
@ -123,14 +79,6 @@ export default {
}
</script>
<style lang="scss">
#filter-posts-header {
display: block;
}
#filter-posts-by-followers-header {
display: block;
}
.categories-menu-item {
text-align: center;
}
@ -150,13 +98,4 @@ export default {
margin: 9px 0px 40px 0px;
}
}
@media only screen and (max-width: 960px) {
#filter-posts-header {
text-align: center;
}
.follow-button {
float: left;
}
}
</style>

View File

@ -19,6 +19,7 @@ describe('FilterPosts.vue', () => {
let allCategoriesButton
let environmentAndNatureButton
let democracyAndPoliticsButton
let happyEmotionButton
beforeEach(() => {
mocks = {
@ -52,6 +53,7 @@ describe('FilterPosts.vue', () => {
'postsFilter/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(),
'postsFilter/RESET_CATEGORIES': jest.fn(),
'postsFilter/TOGGLE_CATEGORY': jest.fn(),
'postsFilter/TOGGLE_EMOTION': jest.fn(),
}
getters = {
'postsFilter/isActive': () => false,
@ -61,6 +63,7 @@ describe('FilterPosts.vue', () => {
},
'postsFilter/filteredCategoryIds': jest.fn(() => []),
'postsFilter/filteredByUsersFollowed': jest.fn(),
'postsFilter/filteredByEmotions': jest.fn(() => []),
}
const openFilterPosts = () => {
const store = new Vuex.Store({ mutations, getters })
@ -120,5 +123,22 @@ describe('FilterPosts.vue', () => {
expect(mutations['postsFilter/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('button.emotions-buttons').at(1)
happyEmotionButton.trigger('click')
expect(mutations['postsFilter/TOGGLE_EMOTION']).toHaveBeenCalledWith({}, 'happy')
})
it('sets the attribute `src` to colorized image', () => {
getters['postsFilter/filteredByEmotions'] = jest.fn(() => ['happy'])
const wrapper = openFilterPosts()
happyEmotionButton = wrapper.findAll('button.emotions-buttons').at(1)
const happyEmotionButtonImage = happyEmotionButton.find('img')
expect(happyEmotionButtonImage.attributes().src).toEqual('/img/svg/emoji/happy_color.svg')
})
})
})
})

View File

@ -11,20 +11,25 @@
<ds-icon size="xx-small" name="angle-down" />
</ds-button>
<template slot="popover">
<filter-posts-menu-items :chunk="chunk" :user="currentUser" />
<ds-container>
<categories-filter-menu-items :chunk="chunk" />
<general-filter-menu-items :user="currentUser" />
</ds-container>
</template>
</dropdown>
</template>
<script>
import _ from 'lodash'
import { chunk } from 'lodash'
import Dropdown from '~/components/Dropdown'
import { mapGetters } from 'vuex'
import FilterPostsMenuItems from '~/components/FilterPosts/FilterPostsMenuItems'
import CategoriesFilterMenuItems from './CategoriesFilterMenuItems'
import GeneralFilterMenuItems from './GeneralFilterMenuItems'
export default {
components: {
Dropdown,
FilterPostsMenuItems,
CategoriesFilterMenuItems,
GeneralFilterMenuItems,
},
props: {
placement: { type: String },
@ -37,7 +42,7 @@ export default {
filterActive: 'postsFilter/isActive',
}),
chunk() {
return _.chunk(this.categories, 2)
return chunk(this.categories, 2)
},
},
}

View File

@ -0,0 +1,110 @@
<template>
<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 :gutter="{ lg: 'large' }">
<ds-flex-item
:width="{ base: '100%', sm: '100%', md: '100%', lg: '10%' }"
class="categories-menu-item"
>
<ds-flex>
<ds-flex-item width="10%" />
<ds-space margin-bottom="xx-small" />
<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="toggleFilteredByFollowed(user.id)"
/>
<ds-space margin-bottom="x-small" />
<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>
<div v-for="emotion in emotionsArray" :key="emotion">
<ds-flex-item :width="{ lg: '100%' }">
<ds-button
size="large"
ghost
@click="toogleFilteredByEmotions(emotion)"
class="emotions-buttons"
>
<img :src="iconPath(emotion)" width="40" />
</ds-button>
<ds-space margin-bottom="x-small" />
<ds-flex-item class="emotions-mobile-space text-center">
<label class="emotions-label">{{ $t(`contribution.emotions-label.${emotion}`) }}</label>
</ds-flex-item>
</ds-flex-item>
</div>
<ds-space margin-bottom="large" />
</ds-flex>
</ds-space>
</template>
<script>
import { mapGetters, mapMutations } from 'vuex'
export default {
props: {
user: { type: Object, required: true },
},
data() {
return {
emotionsArray: ['funny', 'happy', 'surprised', 'cry', 'angry'],
}
},
computed: {
...mapGetters({
filteredByUsersFollowed: 'postsFilter/filteredByUsersFollowed',
filteredByEmotions: 'postsFilter/filteredByEmotions',
}),
},
methods: {
...mapMutations({
toggleFilteredByFollowed: 'postsFilter/TOGGLE_FILTER_BY_FOLLOWED',
toogleFilteredByEmotions: 'postsFilter/TOGGLE_EMOTION',
}),
iconPath(emotion) {
if (this.filteredByEmotions.includes(emotion)) {
return `/img/svg/emoji/${emotion}_color.svg`
}
return `/img/svg/emoji/${emotion}.svg`
},
},
}
</script>
<style lang="scss">
#filter-posts-header {
display: block;
}
#filter-posts-by-followers-header {
display: block;
}
@media only screen and (max-width: 960px) {
#filter-posts-header {
text-align: center;
}
.follow-button {
float: left;
}
}
.text-center {
text-align: center;
}
</style>

View File

@ -27,12 +27,8 @@
<template v-else>
<ds-grid-item :row-span="2" column-span="fullWidth">
<hc-empty icon="docs" />
<ds-text align="center">
{{ $t('index.no-results') }}
</ds-text>
<ds-text align="center">
{{ $t('index.change-filter-settings') }}
</ds-text>
<ds-text align="center">{{ $t('index.no-results') }}</ds-text>
<ds-text align="center">{{ $t('index.change-filter-settings') }}</ds-text>
</ds-grid-item>
</template>
</masonry-grid>

View File

@ -40,6 +40,12 @@ export const mutations = {
if (isEmpty(get(filter, 'categories_some.id_in'))) delete filter.categories_some
state.filter = filter
},
TOGGLE_EMOTION(state, emotion) {
const filter = clone(state.filter)
update(filter, 'emotions_some.emotion_in', emotions => xor(emotions, [emotion]))
if (isEmpty(get(filter, 'emotions_some.emotion_in'))) delete filter.emotions_some
state.filter = filter
},
}
export const getters = {
@ -55,4 +61,7 @@ export const getters = {
filteredByUsersFollowed(state) {
return !!get(state.filter, 'author.followedBy_some.id')
},
filteredByEmotions(state) {
return get(state.filter, 'emotions_some.emotion_in') || []
},
}

View File

@ -43,6 +43,30 @@ describe('getters', () => {
expect(getters.filteredByUsersFollowed(state)).toBe(false)
})
})
describe('filteredByEmotions', () => {
it('returns an emotions array if filter is set', () => {
state = { filter: { emotions_some: { emotion_in: ['sad'] } } }
expect(getters.filteredByEmotions(state)).toEqual(['sad'])
})
it('returns an emotions array even when other filters are set', () => {
state = {
filter: { emotions_some: { emotion_in: ['sad'] }, categories_some: { id_in: [23] } },
}
expect(getters.filteredByEmotions(state)).toEqual(['sad'])
})
it('returns empty array if filter is not set', () => {
state = { filter: {} }
expect(getters.filteredByEmotions(state)).toEqual([])
})
it('returns empty array if another filter is set, but not emotions', () => {
state = { filter: { categories_some: { id_in: [23] } } }
expect(getters.filteredByEmotions(state)).toEqual([])
})
})
})
describe('mutations', () => {