mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge pull request #1490 from Human-Connection/1488-filter-posts-by-emotion
Filter posts by emotions
This commit is contained in:
commit
6cebb61d0f
@ -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),
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')]))
|
||||
|
||||
@ -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>
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
110
webapp/components/FilterPosts/GeneralFilterMenuItems.vue
Normal file
110
webapp/components/FilterPosts/GeneralFilterMenuItems.vue
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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') || []
|
||||
},
|
||||
}
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user