feat: add pagination for search page

- it wasn't really making sense to have one query for all users/posts,
  future hashtags, because we change the first/offset when the user
paginates, which would unneccesarily refetch all other resources.
- the solution was to separate them into their own queries and only
  refetch when the user wants to paginate the resources.
This commit is contained in:
mattwr18 2020-04-02 00:36:26 +02:00
parent 79c1cc02c1
commit e8492b59f4
7 changed files with 224 additions and 19 deletions

View File

@ -64,7 +64,7 @@ Factory.define('basicUser')
password: '1234',
role: 'user',
about: faker.lorem.paragraph,
termsAndConditionsAgreedVersion: '0.0.1',
termsAndConditionsAgreedVersion: '0.0.4',
termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z',
allowEmbedIframes: false,
showShoutsPublicly: false,

View File

@ -929,7 +929,24 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
])
await Promise.all([...Array(30).keys()].map(() => Factory.build('user')))
await Promise.all(
[...Array(30).keys()].map(index => Factory.build('user', { name: `Jenny${index}` })),
)
await Promise.all(
[...Array(30).keys()].map(() =>
Factory.build(
'post',
{ content: `Jenny ${faker.lorem.sentence()}` },
{
categoryIds: ['cat1'],
author: jennyRostock,
image: Factory.build('image', {
url: faker.image.unsplash.objects(),
}),
},
),
),
)
await Promise.all(
[...Array(30).keys()].map(() =>
Factory.build(

View File

@ -87,6 +87,8 @@ export default shield(
findPosts: allow,
findUsers: allow,
searchResults: allow,
searchPosts: allow,
searchUsers: allow,
embed: allow,
Category: allow,
Tag: allow,

View File

@ -5,6 +5,89 @@ import { queryString } from './searches/queryString'
export default {
Query: {
searchPosts: async (_parent, args, context, _resolveInfo) => {
const { query, postsOffset, firstPosts } = args
const { id: userId } = context.user
const postCypher = `
CALL db.index.fulltext.queryNodes('post_fulltext_search', $query)
YIELD node as posts, score
MATCH (posts)<-[:WROTE]-(author:User)
WHERE score >= 0.0
AND NOT (
author.deleted = true OR author.disabled = true
OR posts.deleted = true OR posts.disabled = true
OR (:User {id: $userId})-[:MUTED]->(author)
)
WITH posts, author,
[(posts)<-[:COMMENTS]-(comment:Comment) | comment] as comments,
[(posts)<-[:SHOUTED]-(user:User) | user] as shouter
RETURN posts {
.*,
__typename: labels(posts)[0],
author: properties(author),
commentsCount: toString(size(comments)),
shoutedCount: toString(size(shouter))
}
SKIP $postsOffset
LIMIT $firstPosts
`
const myQuery = queryString(query)
const session = context.driver.session()
const searchResultPromise = session.readTransaction(async transaction => {
const postTransactionResponse = await transaction.run(postCypher, {
query: myQuery,
postsOffset,
firstPosts,
userId,
})
return postTransactionResponse
})
try {
const postResults = await searchResultPromise
log(postResults)
return postResults.records.map(record => record.get('posts'))
} finally {
session.close()
}
},
searchUsers: async (_parent, args, context, _resolveInfo) => {
const { query, usersOffset, firstUsers } = args
const { id: userId } = context.user
const userCypher = `
CALL db.index.fulltext.queryNodes('user_fulltext_search', $query)
YIELD node as users, score
MATCH (users)
WHERE score >= 0.0
AND NOT (users.deleted = true OR users.disabled = true)
RETURN users {.*, __typename: labels(users)[0]}
SKIP $usersOffset
LIMIT $firstUsers
`
const myQuery = queryString(query)
const session = context.driver.session()
const searchResultPromise = session.readTransaction(async transaction => {
const userTransactionResponse = await transaction.run(userCypher, {
query: myQuery,
usersOffset,
firstUsers,
userId,
})
return userTransactionResponse
})
try {
const userResults = await searchResultPromise
log(userResults)
return userResults.records.map(record => record.get('users'))
} finally {
session.close()
}
},
searchResults: async (_parent, args, context, _resolveInfo) => {
const { query, limit } = args
const { id: thisUserId } = context.user

View File

@ -1,5 +1,7 @@
union SearchResult = Post | User
type Query {
searchPosts(query: String!, firstPosts: Int, postsOffset: Int): [Post]!
searchUsers(query: String!, firstUsers: Int, usersOffset: Int): [User]!
searchResults(query: String!, limit: Int = 5): [SearchResult]!
}

View File

@ -9,38 +9,54 @@
icon="tasks"
:message="$t('search.no-results', { search })"
/>
<masonry-grid v-else-if="activeTab === 'Post'">
<masonry-grid-item v-for="resource in activeResources" :key="resource.key">
<post-teaser :post="resource" />
</masonry-grid-item>
</masonry-grid>
<template v-else-if="activeTab === 'Post'">
<masonry-grid>
<masonry-grid-item v-for="resource in activeResources" :key="resource.key">
<post-teaser :post="resource" />
</masonry-grid-item>
</masonry-grid>
<pagination-buttons
:hasNext="hasMorePosts"
:hasPrevious="hasPreviousPosts"
@back="previousPosts"
@next="nextPosts"
/>
</template>
<ul v-else-if="activeTab === 'User'" class="user-list">
<li v-for="resource in activeResources" :key="resource.key" class="item">
<base-card :wideContent="true">
<user-teaser :user="resource" />
</base-card>
</li>
<pagination-buttons
:hasNext="hasMoreUsers"
:hasPrevious="hasPreviousUsers"
@back="previousUsers"
@next="nextUsers"
/>
</ul>
</section>
</div>
</template>
<script>
import { searchQuery } from '~/graphql/Search.js'
import { searchPosts, searchUsers } from '~/graphql/Search.js'
import HcEmpty from '~/components/Empty/Empty'
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid'
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem'
import PostTeaser from '~/components/PostTeaser/PostTeaser'
import TabNavigation from '~/components/_new/generic/TabNavigation/TabNavigation'
import UserTeaser from '~/components/UserTeaser/UserTeaser'
import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons'
export default {
components: {
TabNavigation,
HcEmpty,
MasonryGrid,
MasonryGridItem,
PostTeaser,
TabNavigation,
PaginationButtons,
UserTeaser,
},
props: {
@ -49,10 +65,18 @@ export default {
},
},
data() {
const pageSize = 25
return {
posts: [],
users: [],
activeTab: null,
pageSize,
firstPosts: pageSize,
firstUsers: pageSize,
postsOffset: 0,
usersOffset: 0,
hasMorePosts: false,
hasMoreUsers: false,
}
},
computed: {
@ -75,30 +99,72 @@ export default {
},
]
},
hasPreviousPosts() {
return this.postsOffset > 0
},
hasPreviousUsers() {
return this.usersOffset > 0
},
},
methods: {
switchTab(tab) {
this.activeTab = tab
},
previousPosts() {
this.postsOffset = Math.max(this.postsOffset - this.pageSize, 0)
},
nextPosts() {
this.postsOffset += this.pageSize
},
previousUsers() {
this.usersOffset = Math.max(this.usersOffset - this.pageSize, 0)
},
nextUsers() {
this.usersOffset += this.pageSize
},
},
apollo: {
searchResults: {
searchPosts: {
query() {
return searchQuery
return searchPosts
},
variables() {
const { firstPosts, postsOffset, search } = this
return {
query: this.search,
limit: 37,
query: search,
firstPosts,
postsOffset,
}
},
skip() {
return !this.search
},
update({ searchResults }) {
this.posts = searchResults.filter(result => result.__typename === 'Post')
this.users = searchResults.filter(result => result.__typename === 'User')
if (searchResults.length) this.activeTab = searchResults[0].__typename
update({ searchPosts }) {
this.posts = searchPosts
this.hasMorePosts = this.posts.length >= this.pageSize
if (searchPosts.length) this.activeTab = 'Post'
},
fetchPolicy: 'cache-and-network',
},
searchUsers: {
query() {
return searchUsers
},
variables() {
const { firstUsers, usersOffset, search } = this
return {
query: search,
firstUsers,
usersOffset,
}
},
skip() {
return !this.search
},
update({ searchUsers }) {
this.users = searchUsers
this.hasMoreUsers = this.users.length >= this.pageSize
if (!searchPosts.length && searchUsers.length) this.activeTab = 'User'
},
fetchPolicy: 'cache-and-network',
},

View File

@ -6,8 +6,13 @@ export const searchQuery = gql`
${postFragment}
${tagsCategoriesAndPinnedFragment}
query($query: String!, $limit: Int = 5) {
searchResults(query: $query, limit: $limit) {
query($query: String!, $firstPosts: Int, $firstUsers: Int, $offset: Int) {
searchResults(
query: $query
firstPosts: $firstPosts
firstUsers: $firstUsers
offset: $offset
) {
__typename
... on Post {
...post
@ -24,3 +29,33 @@ export const searchQuery = gql`
}
}
`
export const searchPosts = gql`
${userFragment}
${postFragment}
${tagsCategoriesAndPinnedFragment}
query($query: String!, $firstPosts: Int, $postsOffset: Int) {
searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) {
__typename
...post
...tagsCategoriesAndPinned
commentsCount
shoutedCount
author {
...user
}
}
}
`
export const searchUsers = gql`
${userFragment}
query($query: String!, $firstUsers: Int, $usersOffset: Int) {
searchUsers(query: $query, firstUsers: $firstUsers, usersOffset: $usersOffset) {
__typename
...user
}
}
`