mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #762 from Human-Connection/refactor_filter_bubble
Refactor filter bubble
This commit is contained in:
commit
bc8dd22a91
@ -1,12 +0,0 @@
|
||||
import replaceParams from './replaceParams'
|
||||
|
||||
const replaceFilterBubbleParams = async (resolve, root, args, context, resolveInfo) => {
|
||||
args = await replaceParams(args, context)
|
||||
return resolve(root, args, context, resolveInfo)
|
||||
}
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
Post: replaceFilterBubbleParams,
|
||||
},
|
||||
}
|
||||
@ -5,6 +5,7 @@ import Factory from '../../seed/factories'
|
||||
const factory = Factory()
|
||||
|
||||
const currentUserParams = {
|
||||
id: 'u1',
|
||||
email: 'you@example.org',
|
||||
name: 'This is you',
|
||||
password: '1234',
|
||||
@ -41,7 +42,7 @@ afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('FilterBubble middleware', () => {
|
||||
describe('Filter posts by author is followed by sb.', () => {
|
||||
describe('given an authenticated user', () => {
|
||||
let authenticatedClient
|
||||
|
||||
@ -52,7 +53,7 @@ describe('FilterBubble middleware', () => {
|
||||
|
||||
describe('no filter bubble', () => {
|
||||
it('returns all posts', async () => {
|
||||
const query = '{ Post( filterBubble: {}) { title } }'
|
||||
const query = '{ Post(filter: { }) { title } }'
|
||||
const expected = {
|
||||
Post: [
|
||||
{ title: 'This is some random post' },
|
||||
@ -65,7 +66,7 @@ describe('FilterBubble middleware', () => {
|
||||
|
||||
describe('filtering for posts of followed users only', () => {
|
||||
it('returns only posts authored by followed users', async () => {
|
||||
const query = '{ Post( filterBubble: { author: following }) { title } }'
|
||||
const query = '{ Post( filter: { author: { followedBy_some: { id: "u1" } } }) { title } }'
|
||||
const expected = {
|
||||
Post: [{ title: 'This is the post of a followed user' }],
|
||||
}
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
import { UserInputError } from 'apollo-server'
|
||||
|
||||
export default async function replaceParams(args, context) {
|
||||
const { author = 'all' } = args.filterBubble || {}
|
||||
const { user } = context
|
||||
|
||||
if (author === 'following') {
|
||||
if (!user)
|
||||
throw new UserInputError(
|
||||
"You are unauthenticated - I don't know any users you are following.",
|
||||
)
|
||||
|
||||
const session = context.driver.session()
|
||||
let { records } = await session.run(
|
||||
'MATCH(followed:User)<-[:FOLLOWS]-(u {id: $userId}) RETURN followed.id',
|
||||
{ userId: context.user.id },
|
||||
)
|
||||
const followedIds = records.map(record => record.get('followed.id'))
|
||||
|
||||
// carefully override `id_in`
|
||||
args.filter = args.filter || {}
|
||||
args.filter.author = args.filter.author || {}
|
||||
args.filter.author.id_in = followedIds
|
||||
|
||||
session.close()
|
||||
}
|
||||
|
||||
delete args.filterBubble
|
||||
|
||||
return args
|
||||
}
|
||||
@ -1,129 +0,0 @@
|
||||
import replaceParams from './replaceParams.js'
|
||||
|
||||
describe('replaceParams', () => {
|
||||
let args
|
||||
let context
|
||||
let run
|
||||
|
||||
let action = () => {
|
||||
return replaceParams(args, context)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
args = {}
|
||||
run = jest.fn().mockResolvedValue({
|
||||
records: [{ get: () => 1 }, { get: () => 2 }, { get: () => 3 }],
|
||||
})
|
||||
context = {
|
||||
driver: {
|
||||
session: () => {
|
||||
return {
|
||||
run,
|
||||
close: () => {},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('args == ', () => {
|
||||
describe('{}', () => {
|
||||
it('does not crash', async () => {
|
||||
await expect(action()).resolves.toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('unauthenticated user', () => {
|
||||
beforeEach(() => {
|
||||
context.user = null
|
||||
})
|
||||
|
||||
describe('{ filterBubble: { author: following } }', () => {
|
||||
it('throws error', async () => {
|
||||
args = { filterBubble: { author: 'following' } }
|
||||
await expect(action()).rejects.toThrow('You are unauthenticated')
|
||||
})
|
||||
})
|
||||
|
||||
describe('{ filterBubble: { author: all } }', () => {
|
||||
it('removes filterBubble param', async () => {
|
||||
const expected = {}
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('does not make database calls', async () => {
|
||||
await action()
|
||||
expect(run).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated user', () => {
|
||||
beforeEach(() => {
|
||||
context.user = { id: 'u4711' }
|
||||
})
|
||||
|
||||
describe('{ filterBubble: { author: following } }', () => {
|
||||
beforeEach(() => {
|
||||
args = { filterBubble: { author: 'following' } }
|
||||
})
|
||||
|
||||
it('returns args object with resolved ids of followed users', async () => {
|
||||
const expected = { filter: { author: { id_in: [1, 2, 3] } } }
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('makes database calls', async () => {
|
||||
await action()
|
||||
expect(run).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
describe('given any additional filter args', () => {
|
||||
describe('merges', () => {
|
||||
it('empty filter object', async () => {
|
||||
args.filter = {}
|
||||
const expected = { filter: { author: { id_in: [1, 2, 3] } } }
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('filter.title', async () => {
|
||||
args.filter = { title: 'bla' }
|
||||
const expected = { filter: { title: 'bla', author: { id_in: [1, 2, 3] } } }
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('filter.author', async () => {
|
||||
args.filter = { author: { name: 'bla' } }
|
||||
const expected = { filter: { author: { name: 'bla', id_in: [1, 2, 3] } } }
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('{ filterBubble: { } }', () => {
|
||||
it('removes filterBubble param', async () => {
|
||||
const expected = {}
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('does not make database calls', async () => {
|
||||
await action()
|
||||
expect(run).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('{ filterBubble: { author: all } }', () => {
|
||||
it('removes filterBubble param', async () => {
|
||||
const expected = {}
|
||||
await expect(action()).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('does not make database calls', async () => {
|
||||
await action()
|
||||
expect(run).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -13,7 +13,6 @@ import includedFields from './includedFieldsMiddleware'
|
||||
import orderBy from './orderByMiddleware'
|
||||
import validation from './validation'
|
||||
import notifications from './notifications'
|
||||
import filterBubble from './filterBubble/filterBubble'
|
||||
|
||||
export default schema => {
|
||||
const middlewares = {
|
||||
@ -31,13 +30,11 @@ export default schema => {
|
||||
user: user,
|
||||
includedFields: includedFields,
|
||||
orderBy: orderBy,
|
||||
filterBubble: filterBubble,
|
||||
}
|
||||
|
||||
let order = [
|
||||
'permissions',
|
||||
'activityPub',
|
||||
'filterBubble',
|
||||
'password',
|
||||
'dateTime',
|
||||
'validation',
|
||||
|
||||
@ -1,40 +1,3 @@
|
||||
enum FilterBubbleAuthorEnum {
|
||||
following
|
||||
all
|
||||
}
|
||||
|
||||
input FilterBubble {
|
||||
author: FilterBubbleAuthorEnum
|
||||
}
|
||||
|
||||
type Query {
|
||||
Post(
|
||||
id: ID
|
||||
activityId: String
|
||||
objectId: String
|
||||
title: String
|
||||
slug: String
|
||||
content: String
|
||||
contentExcerpt: String
|
||||
image: String
|
||||
imageUpload: Upload
|
||||
visibility: Visibility
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
commentsCount: Int
|
||||
shoutedCount: Int
|
||||
shoutedByCurrentUser: Boolean
|
||||
_id: String
|
||||
first: Int
|
||||
offset: Int
|
||||
orderBy: [_PostOrdering]
|
||||
filter: _PostFilter
|
||||
filterBubble: FilterBubble
|
||||
): [Post]
|
||||
}
|
||||
|
||||
type Post {
|
||||
id: ID!
|
||||
activityId: String
|
||||
|
||||
@ -9,9 +9,11 @@ localVue.use(Styleguide)
|
||||
describe('FilterMenu.vue', () => {
|
||||
let wrapper
|
||||
let mocks
|
||||
let propsData
|
||||
|
||||
const createWrapper = mountMethod => {
|
||||
return mountMethod(FilterMenu, {
|
||||
propsData,
|
||||
mocks,
|
||||
localVue,
|
||||
})
|
||||
@ -19,35 +21,48 @@ describe('FilterMenu.vue', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mocks = { $t: () => {} }
|
||||
propsData = {}
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
describe('given a user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper(mount)
|
||||
propsData = {
|
||||
user: {
|
||||
id: '4711',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('renders a card', () => {
|
||||
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()
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper(mount)
|
||||
})
|
||||
|
||||
it('toggles filterBubble.author property', () => {
|
||||
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
|
||||
expect(wrapper.emitted('changeFilterBubble')[0]).toEqual([{ author: 'following' }])
|
||||
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
|
||||
expect(wrapper.emitted('changeFilterBubble')[1]).toEqual([{ author: 'all' }])
|
||||
it('renders a card', () => {
|
||||
expect(wrapper.is('.ds-card')).toBe(true)
|
||||
})
|
||||
|
||||
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 "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('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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
<ds-button
|
||||
name="filter-by-followed-authors-only"
|
||||
icon="user-plus"
|
||||
:primary="onlyFollowed"
|
||||
:primary="!!filterAuthorIsFollowedById"
|
||||
@click="toggleOnlyFollowed"
|
||||
/>
|
||||
</div>
|
||||
@ -22,24 +22,30 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
user: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
// We have to fix styleguide here. It uses .includes wich will always be
|
||||
// false for arrays of objects.
|
||||
return {
|
||||
filterBubble: {
|
||||
author: 'all',
|
||||
},
|
||||
filter: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
onlyFollowed() {
|
||||
return this.filterBubble.author === 'following'
|
||||
filterAuthorIsFollowedById() {
|
||||
const { author = {} } = this.filter
|
||||
/* eslint-disable camelcase */
|
||||
const { followedBy_some = {} } = author
|
||||
const { id } = followedBy_some
|
||||
/* eslint-enable */
|
||||
return id
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleOnlyFollowed() {
|
||||
this.filterBubble.author = this.onlyFollowed ? 'all' : 'following'
|
||||
this.$emit('changeFilterBubble', this.filterBubble)
|
||||
this.filter = this.filterAuthorIsFollowedById
|
||||
? {}
|
||||
: { author: { followedBy_some: { id: this.user.id } } }
|
||||
this.$emit('changeFilterBubble', this.filter)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<ds-flex :width="{ base: '100%' }" gutter="base">
|
||||
<ds-flex-item>
|
||||
<filter-menu @changeFilterBubble="changeFilterBubble" />
|
||||
<filter-menu :user="currentUser" @changeFilterBubble="changeFilterBubble" />
|
||||
</ds-flex-item>
|
||||
<hc-post-card
|
||||
v-for="(post, index) in uniq(Post)"
|
||||
@ -32,6 +32,7 @@ import gql from 'graphql-tag'
|
||||
import uniqBy from 'lodash/uniqBy'
|
||||
import HcPostCard from '~/components/PostCard'
|
||||
import HcLoadMore from '~/components/LoadMore.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -45,10 +46,13 @@ export default {
|
||||
Post: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
filterBubble: { author: 'all' },
|
||||
filter: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'auth/user',
|
||||
}),
|
||||
tags() {
|
||||
return this.Post ? this.Post[0].tags.map(tag => tag.name) : '-'
|
||||
},
|
||||
@ -57,8 +61,8 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeFilterBubble(filterBubble) {
|
||||
this.filterBubble = filterBubble
|
||||
changeFilterBubble(filter) {
|
||||
this.filter = filter
|
||||
this.$apollo.queries.Post.refresh()
|
||||
},
|
||||
uniq(items, field = 'id') {
|
||||
@ -76,7 +80,7 @@ export default {
|
||||
this.page++
|
||||
this.$apollo.queries.Post.fetchMore({
|
||||
variables: {
|
||||
filterBubble: this.filterBubble,
|
||||
filter: this.filter,
|
||||
first: this.pageSize,
|
||||
offset: this.offset,
|
||||
},
|
||||
@ -102,8 +106,8 @@ export default {
|
||||
Post: {
|
||||
query() {
|
||||
return gql(`
|
||||
query Post($filterBubble: FilterBubble, $first: Int, $offset: Int) {
|
||||
Post(filterBubble: $filterBubble, first: $first, offset: $offset) {
|
||||
query Post($filter: _PostFilter, $first: Int, $offset: Int) {
|
||||
Post(filter: $filter, first: $first, offset: $offset) {
|
||||
id
|
||||
title
|
||||
contentExcerpt
|
||||
@ -146,7 +150,7 @@ export default {
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
filterBubble: this.filterBubble,
|
||||
filter: this.filter,
|
||||
first: this.pageSize,
|
||||
offset: 0,
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user