mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge branch '2144-Add_Search_Results_Page' of https://github.com/Human-Connection/Human-Connection into HC-2144-Add_Search_Results_Page
# Conflicts: # webapp/pages/profile/_id/_slug.vue
This commit is contained in:
commit
7dbc833e22
@ -931,6 +931,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
|||||||
const additionalUsers = await Promise.all(
|
const additionalUsers = await Promise.all(
|
||||||
[...Array(30).keys()].map(() => Factory.build('user')),
|
[...Array(30).keys()].map(() => Factory.build('user')),
|
||||||
)
|
)
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
additionalUsers.map(async (user) => {
|
additionalUsers.map(async (user) => {
|
||||||
await jennyRostock.relateTo(user, 'following')
|
await jennyRostock.relateTo(user, 'following')
|
||||||
@ -938,6 +939,26 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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(
|
await Promise.all(
|
||||||
[...Array(30).keys()].map(() =>
|
[...Array(30).keys()].map(() =>
|
||||||
Factory.build(
|
Factory.build(
|
||||||
|
|||||||
@ -86,7 +86,10 @@ export default shield(
|
|||||||
'*': deny,
|
'*': deny,
|
||||||
findPosts: allow,
|
findPosts: allow,
|
||||||
findUsers: allow,
|
findUsers: allow,
|
||||||
findResources: allow,
|
searchResults: allow,
|
||||||
|
searchPosts: allow,
|
||||||
|
searchUsers: allow,
|
||||||
|
searchHashtags: allow,
|
||||||
embed: allow,
|
embed: allow,
|
||||||
Category: allow,
|
Category: allow,
|
||||||
Tag: allow,
|
Tag: allow,
|
||||||
|
|||||||
@ -3,90 +3,200 @@ import { queryString } from './searches/queryString'
|
|||||||
|
|
||||||
// see http://lucene.apache.org/core/8_3_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package.description
|
// see http://lucene.apache.org/core/8_3_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package.description
|
||||||
|
|
||||||
|
const cypherTemplate = (setup) => `
|
||||||
|
CALL db.index.fulltext.queryNodes('${setup.fulltextIndex}', $query)
|
||||||
|
YIELD node AS resource, score
|
||||||
|
${setup.match}
|
||||||
|
${setup.whereClause}
|
||||||
|
${setup.withClause}
|
||||||
|
RETURN
|
||||||
|
${setup.returnClause}
|
||||||
|
AS result
|
||||||
|
SKIP $skip
|
||||||
|
${setup.limit}
|
||||||
|
`
|
||||||
|
|
||||||
|
const simpleWhereClause =
|
||||||
|
'WHERE score >= 0.0 AND NOT (resource.deleted = true OR resource.disabled = true)'
|
||||||
|
|
||||||
|
const postWhereClause = `WHERE score >= 0.0
|
||||||
|
AND NOT (
|
||||||
|
author.deleted = true OR author.disabled = true
|
||||||
|
OR resource.deleted = true OR resource.disabled = true
|
||||||
|
OR (:User {id: $userId})-[:MUTED]->(author)
|
||||||
|
)`
|
||||||
|
|
||||||
|
const searchPostsSetup = {
|
||||||
|
fulltextIndex: 'post_fulltext_search',
|
||||||
|
match: 'MATCH (resource:Post)<-[:WROTE]-(author:User)',
|
||||||
|
whereClause: postWhereClause,
|
||||||
|
withClause: `WITH resource, author,
|
||||||
|
[(resource)<-[:COMMENTS]-(comment:Comment) | comment] AS comments,
|
||||||
|
[(resource)<-[:SHOUTED]-(user:User) | user] AS shouter`,
|
||||||
|
returnClause: `resource {
|
||||||
|
.*,
|
||||||
|
__typename: labels(resource)[0],
|
||||||
|
author: properties(author),
|
||||||
|
commentsCount: toString(size(comments)),
|
||||||
|
shoutedCount: toString(size(shouter))
|
||||||
|
}`,
|
||||||
|
limit: 'LIMIT $limit',
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchUsersSetup = {
|
||||||
|
fulltextIndex: 'user_fulltext_search',
|
||||||
|
match: 'MATCH (resource:User)',
|
||||||
|
whereClause: simpleWhereClause,
|
||||||
|
withClause: '',
|
||||||
|
returnClause: 'resource {.*, __typename: labels(resource)[0]}',
|
||||||
|
limit: 'LIMIT $limit',
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchHashtagsSetup = {
|
||||||
|
fulltextIndex: 'tag_fulltext_search',
|
||||||
|
match: 'MATCH (resource:Tag)',
|
||||||
|
whereClause: simpleWhereClause,
|
||||||
|
withClause: '',
|
||||||
|
returnClause: 'resource {.*, __typename: labels(resource)[0]}',
|
||||||
|
limit: 'LIMIT $limit',
|
||||||
|
}
|
||||||
|
|
||||||
|
const countSetup = {
|
||||||
|
returnClause: 'toString(size(collect(resource)))',
|
||||||
|
limit: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const countUsersSetup = {
|
||||||
|
...searchUsersSetup,
|
||||||
|
...countSetup,
|
||||||
|
}
|
||||||
|
const countPostsSetup = {
|
||||||
|
...searchPostsSetup,
|
||||||
|
...countSetup,
|
||||||
|
}
|
||||||
|
const countHashtagsSetup = {
|
||||||
|
...searchHashtagsSetup,
|
||||||
|
...countSetup,
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchResultPromise = async (session, setup, params) => {
|
||||||
|
return session.readTransaction(async (transaction) => {
|
||||||
|
return transaction.run(cypherTemplate(setup), params)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchResultCallback = (result) => {
|
||||||
|
return result.records.map((r) => r.get('result'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const countResultCallback = (result) => {
|
||||||
|
return result.records[0].get('result')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSearchResults = async (context, setup, params, resultCallback = searchResultCallback) => {
|
||||||
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
|
const results = await searchResultPromise(session, setup, params)
|
||||||
|
log(results)
|
||||||
|
return resultCallback(results)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiSearchMap = [
|
||||||
|
{ symbol: '!', setup: searchPostsSetup, resultName: 'posts' },
|
||||||
|
{ symbol: '@', setup: searchUsersSetup, resultName: 'users' },
|
||||||
|
{ symbol: '#', setup: searchHashtagsSetup, resultName: 'hashtags' },
|
||||||
|
]
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
findResources: async (_parent, args, context, _resolveInfo) => {
|
searchPosts: async (_parent, args, context, _resolveInfo) => {
|
||||||
|
const { query, postsOffset, firstPosts } = args
|
||||||
|
const { id: userId } = context.user
|
||||||
|
|
||||||
|
return {
|
||||||
|
postCount: getSearchResults(
|
||||||
|
context,
|
||||||
|
countPostsSetup,
|
||||||
|
{
|
||||||
|
query: queryString(query),
|
||||||
|
skip: 0,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
countResultCallback,
|
||||||
|
),
|
||||||
|
posts: getSearchResults(context, searchPostsSetup, {
|
||||||
|
query: queryString(query),
|
||||||
|
skip: postsOffset,
|
||||||
|
limit: firstPosts,
|
||||||
|
userId,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
searchUsers: async (_parent, args, context, _resolveInfo) => {
|
||||||
|
const { query, usersOffset, firstUsers } = args
|
||||||
|
return {
|
||||||
|
userCount: getSearchResults(
|
||||||
|
context,
|
||||||
|
countUsersSetup,
|
||||||
|
{
|
||||||
|
query: queryString(query),
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
countResultCallback,
|
||||||
|
),
|
||||||
|
users: getSearchResults(context, searchUsersSetup, {
|
||||||
|
query: queryString(query),
|
||||||
|
skip: usersOffset,
|
||||||
|
limit: firstUsers,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
searchHashtags: async (_parent, args, context, _resolveInfo) => {
|
||||||
|
const { query, hashtagsOffset, firstHashtags } = args
|
||||||
|
return {
|
||||||
|
hashtagCount: getSearchResults(
|
||||||
|
context,
|
||||||
|
countHashtagsSetup,
|
||||||
|
{
|
||||||
|
query: queryString(query),
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
countResultCallback,
|
||||||
|
),
|
||||||
|
hashtags: getSearchResults(context, searchHashtagsSetup, {
|
||||||
|
query: queryString(query),
|
||||||
|
skip: hashtagsOffset,
|
||||||
|
limit: firstHashtags,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
searchResults: async (_parent, args, context, _resolveInfo) => {
|
||||||
const { query, limit } = args
|
const { query, limit } = args
|
||||||
const { id: thisUserId } = context.user
|
const { id: userId } = context.user
|
||||||
|
|
||||||
const postCypher = `
|
const searchType = query.replace(/^([!@#]?).*$/, '$1')
|
||||||
CALL db.index.fulltext.queryNodes('post_fulltext_search', $query)
|
const searchString = query.replace(/^([!@#])/, '')
|
||||||
YIELD node as resource, score
|
|
||||||
MATCH (resource)<-[:WROTE]-(author:User)
|
const params = {
|
||||||
WHERE score >= 0.0
|
query: queryString(searchString),
|
||||||
AND NOT (
|
skip: 0,
|
||||||
author.deleted = true OR author.disabled = true
|
limit,
|
||||||
OR resource.deleted = true OR resource.disabled = true
|
userId,
|
||||||
OR (:User {id: $thisUserId})-[:MUTED]->(author)
|
|
||||||
)
|
|
||||||
WITH resource, author,
|
|
||||||
[(resource)<-[:COMMENTS]-(comment:Comment) | comment] as comments,
|
|
||||||
[(resource)<-[:SHOUTED]-(user:User) | user] as shouter
|
|
||||||
RETURN resource {
|
|
||||||
.*,
|
|
||||||
__typename: labels(resource)[0],
|
|
||||||
author: properties(author),
|
|
||||||
commentsCount: toString(size(comments)),
|
|
||||||
shoutedCount: toString(size(shouter))
|
|
||||||
}
|
}
|
||||||
LIMIT $limit
|
|
||||||
`
|
|
||||||
|
|
||||||
const userCypher = `
|
if (searchType === '')
|
||||||
CALL db.index.fulltext.queryNodes('user_fulltext_search', $query)
|
return [
|
||||||
YIELD node as resource, score
|
...(await getSearchResults(context, searchPostsSetup, params)),
|
||||||
MATCH (resource)
|
...(await getSearchResults(context, searchUsersSetup, params)),
|
||||||
WHERE score >= 0.0
|
...(await getSearchResults(context, searchHashtagsSetup, params)),
|
||||||
AND NOT (resource.deleted = true OR resource.disabled = true)
|
]
|
||||||
RETURN resource {.*, __typename: labels(resource)[0]}
|
|
||||||
LIMIT $limit
|
|
||||||
`
|
|
||||||
const tagCypher = `
|
|
||||||
CALL db.index.fulltext.queryNodes('tag_fulltext_search', $query)
|
|
||||||
YIELD node as resource, score
|
|
||||||
MATCH (resource)
|
|
||||||
WHERE score >= 0.0
|
|
||||||
AND NOT (resource.deleted = true OR resource.disabled = true)
|
|
||||||
RETURN resource {.*, __typename: labels(resource)[0]}
|
|
||||||
LIMIT $limit
|
|
||||||
`
|
|
||||||
|
|
||||||
const myQuery = queryString(query)
|
params.limit = 15
|
||||||
|
const type = multiSearchMap.find((obj) => obj.symbol === searchType)
|
||||||
const session = context.driver.session()
|
return getSearchResults(context, type.setup, params)
|
||||||
const searchResultPromise = session.readTransaction(async (transaction) => {
|
|
||||||
const postTransactionResponse = transaction.run(postCypher, {
|
|
||||||
query: myQuery,
|
|
||||||
limit,
|
|
||||||
thisUserId,
|
|
||||||
})
|
|
||||||
const userTransactionResponse = transaction.run(userCypher, {
|
|
||||||
query: myQuery,
|
|
||||||
limit,
|
|
||||||
thisUserId,
|
|
||||||
})
|
|
||||||
const tagTransactionResponse = transaction.run(tagCypher, {
|
|
||||||
query: myQuery,
|
|
||||||
limit,
|
|
||||||
})
|
|
||||||
return Promise.all([
|
|
||||||
postTransactionResponse,
|
|
||||||
userTransactionResponse,
|
|
||||||
tagTransactionResponse,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [postResults, userResults, tagResults] = await searchResultPromise
|
|
||||||
log(postResults)
|
|
||||||
log(userResults)
|
|
||||||
log(tagResults)
|
|
||||||
return [...postResults.records, ...userResults.records, ...tagResults.records].map((r) =>
|
|
||||||
r.get('resource'),
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
session.close()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ afterAll(async () => {
|
|||||||
|
|
||||||
const searchQuery = gql`
|
const searchQuery = gql`
|
||||||
query($query: String!) {
|
query($query: String!) {
|
||||||
findResources(query: $query, limit: 5) {
|
searchResults(query: $query, limit: 5) {
|
||||||
__typename
|
__typename
|
||||||
... on Post {
|
... on Post {
|
||||||
id
|
id
|
||||||
@ -47,6 +47,21 @@ const searchQuery = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const searchPostQuery = gql`
|
||||||
|
query($query: String!, $firstPosts: Int, $postsOffset: Int) {
|
||||||
|
searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) {
|
||||||
|
postCount
|
||||||
|
posts {
|
||||||
|
__typename
|
||||||
|
id
|
||||||
|
title
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
describe('resolvers/searches', () => {
|
describe('resolvers/searches', () => {
|
||||||
let variables
|
let variables
|
||||||
|
|
||||||
@ -65,7 +80,7 @@ describe('resolvers/searches', () => {
|
|||||||
variables = { query: 'John' }
|
variables = { query: 'John' }
|
||||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
findResources: [
|
searchResults: [
|
||||||
{
|
{
|
||||||
id: 'a-user',
|
id: 'a-user',
|
||||||
name: 'John Doe',
|
name: 'John Doe',
|
||||||
@ -95,7 +110,7 @@ describe('resolvers/searches', () => {
|
|||||||
variables = { query: 'beitrag' }
|
variables = { query: 'beitrag' }
|
||||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
findResources: [
|
searchResults: [
|
||||||
{
|
{
|
||||||
__typename: 'Post',
|
__typename: 'Post',
|
||||||
id: 'a-post',
|
id: 'a-post',
|
||||||
@ -114,7 +129,7 @@ describe('resolvers/searches', () => {
|
|||||||
variables = { query: 'BEITRAG' }
|
variables = { query: 'BEITRAG' }
|
||||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
findResources: [
|
searchResults: [
|
||||||
{
|
{
|
||||||
__typename: 'Post',
|
__typename: 'Post',
|
||||||
id: 'a-post',
|
id: 'a-post',
|
||||||
@ -132,7 +147,7 @@ describe('resolvers/searches', () => {
|
|||||||
it('returns empty search results', async () => {
|
it('returns empty search results', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
query({ query: searchQuery, variables: { query: 'Unfug' } }),
|
query({ query: searchQuery, variables: { query: 'Unfug' } }),
|
||||||
).resolves.toMatchObject({ data: { findResources: [] } })
|
).resolves.toMatchObject({ data: { searchResults: [] } })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -189,7 +204,7 @@ und hinter tausend Stäben keine Welt.`,
|
|||||||
variables = { query: 'beitrag' }
|
variables = { query: 'beitrag' }
|
||||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
findResources: expect.arrayContaining([
|
searchResults: expect.arrayContaining([
|
||||||
{
|
{
|
||||||
__typename: 'Post',
|
__typename: 'Post',
|
||||||
id: 'a-post',
|
id: 'a-post',
|
||||||
@ -216,7 +231,7 @@ und hinter tausend Stäben keine Welt.`,
|
|||||||
variables = { query: 'tee-ei' }
|
variables = { query: 'tee-ei' }
|
||||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
findResources: [
|
searchResults: [
|
||||||
{
|
{
|
||||||
__typename: 'Post',
|
__typename: 'Post',
|
||||||
id: 'g-post',
|
id: 'g-post',
|
||||||
@ -235,7 +250,7 @@ und hinter tausend Stäben keine Welt.`,
|
|||||||
variables = { query: '„teeei“' }
|
variables = { query: '„teeei“' }
|
||||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
findResources: [
|
searchResults: [
|
||||||
{
|
{
|
||||||
__typename: 'Post',
|
__typename: 'Post',
|
||||||
id: 'g-post',
|
id: 'g-post',
|
||||||
@ -256,7 +271,7 @@ und hinter tausend Stäben keine Welt.`,
|
|||||||
variables = { query: '(a - b)²' }
|
variables = { query: '(a - b)²' }
|
||||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
findResources: [
|
searchResults: [
|
||||||
{
|
{
|
||||||
__typename: 'Post',
|
__typename: 'Post',
|
||||||
id: 'c-post',
|
id: 'c-post',
|
||||||
@ -277,7 +292,7 @@ und hinter tausend Stäben keine Welt.`,
|
|||||||
variables = { query: '(a-b)²' }
|
variables = { query: '(a-b)²' }
|
||||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
findResources: [
|
searchResults: [
|
||||||
{
|
{
|
||||||
__typename: 'Post',
|
__typename: 'Post',
|
||||||
id: 'c-post',
|
id: 'c-post',
|
||||||
@ -298,7 +313,7 @@ und hinter tausend Stäben keine Welt.`,
|
|||||||
variables = { query: '+ b² 2.' }
|
variables = { query: '+ b² 2.' }
|
||||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
findResources: [
|
searchResults: [
|
||||||
{
|
{
|
||||||
__typename: 'Post',
|
__typename: 'Post',
|
||||||
id: 'c-post',
|
id: 'c-post',
|
||||||
@ -321,7 +336,7 @@ und hinter tausend Stäben keine Welt.`,
|
|||||||
variables = { query: 'der panther' }
|
variables = { query: 'der panther' }
|
||||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
findResources: [
|
searchResults: [
|
||||||
{
|
{
|
||||||
__typename: 'Post',
|
__typename: 'Post',
|
||||||
id: 'd-post',
|
id: 'd-post',
|
||||||
@ -349,7 +364,7 @@ und hinter tausend Stäben keine Welt.`,
|
|||||||
variables = { query: 'Vorü Subs' }
|
variables = { query: 'Vorü Subs' }
|
||||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
findResources: expect.arrayContaining([
|
searchResults: expect.arrayContaining([
|
||||||
{
|
{
|
||||||
__typename: 'Post',
|
__typename: 'Post',
|
||||||
id: 'd-post',
|
id: 'd-post',
|
||||||
@ -395,7 +410,7 @@ und hinter tausend Stäben keine Welt.`,
|
|||||||
variables = { query: '-maria-' }
|
variables = { query: '-maria-' }
|
||||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
findResources: expect.arrayContaining([
|
searchResults: expect.arrayContaining([
|
||||||
{
|
{
|
||||||
__typename: 'User',
|
__typename: 'User',
|
||||||
id: 'c-user',
|
id: 'c-user',
|
||||||
@ -416,6 +431,128 @@ und hinter tausend Stäben keine Welt.`,
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('adding a user and a hashtag with a name that is content of a post', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
Factory.build('user', {
|
||||||
|
id: 'f-user',
|
||||||
|
name: 'Peter Panther',
|
||||||
|
slug: 'peter-panther',
|
||||||
|
}),
|
||||||
|
await Factory.build('tag', { id: 'Panther' }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('query the word that contains the post, the hashtag and the name of the user', () => {
|
||||||
|
it('finds the user, the post and the hashtag', async () => {
|
||||||
|
variables = { query: 'panther' }
|
||||||
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
searchResults: expect.arrayContaining([
|
||||||
|
{
|
||||||
|
__typename: 'User',
|
||||||
|
id: 'f-user',
|
||||||
|
name: 'Peter Panther',
|
||||||
|
slug: 'peter-panther',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Post',
|
||||||
|
id: 'd-post',
|
||||||
|
title: 'Der Panther',
|
||||||
|
content: `Sein Blick ist vom Vorübergehn der Stäbe<br>
|
||||||
|
so müd geworden, daß er nichts mehr hält.<br>
|
||||||
|
Ihm ist, als ob es tausend Stäbe gäbe<br>
|
||||||
|
und hinter tausend Stäben keine Welt.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Tag',
|
||||||
|
id: 'Panther',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('@query the word that contains the post, the hashtag and the name of the user', () => {
|
||||||
|
it('only finds the user', async () => {
|
||||||
|
variables = { query: '@panther' }
|
||||||
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
searchResults: expect.not.arrayContaining([
|
||||||
|
{
|
||||||
|
__typename: 'Post',
|
||||||
|
id: 'd-post',
|
||||||
|
title: 'Der Panther',
|
||||||
|
content: `Sein Blick ist vom Vorübergehn der Stäbe<br>
|
||||||
|
so müd geworden, daß er nichts mehr hält.<br>
|
||||||
|
Ihm ist, als ob es tausend Stäbe gäbe<br>
|
||||||
|
und hinter tausend Stäben keine Welt.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Tag',
|
||||||
|
id: 'Panther',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('!query the word that contains the post, the hashtag and the name of the user', () => {
|
||||||
|
it('only finds the post', async () => {
|
||||||
|
variables = { query: '!panther' }
|
||||||
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
searchResults: expect.not.arrayContaining([
|
||||||
|
{
|
||||||
|
__typename: 'User',
|
||||||
|
id: 'f-user',
|
||||||
|
name: 'Peter Panther',
|
||||||
|
slug: 'peter-panther',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Tag',
|
||||||
|
id: 'Panther',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#query the word that contains the post, the hashtag and the name of the user', () => {
|
||||||
|
it('only finds the hashtag', async () => {
|
||||||
|
variables = { query: '#panther' }
|
||||||
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
searchResults: expect.not.arrayContaining([
|
||||||
|
{
|
||||||
|
__typename: 'User',
|
||||||
|
id: 'f-user',
|
||||||
|
name: 'Peter Panther',
|
||||||
|
slug: 'peter-panther',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Post',
|
||||||
|
id: 'd-post',
|
||||||
|
title: 'Der Panther',
|
||||||
|
content: `Sein Blick ist vom Vorübergehn der Stäbe<br>
|
||||||
|
so müd geworden, daß er nichts mehr hält.<br>
|
||||||
|
Ihm ist, als ob es tausend Stäbe gäbe<br>
|
||||||
|
und hinter tausend Stäben keine Welt.`,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('adding a post, written by a user who is muted by the authenticated user', () => {
|
describe('adding a post, written by a user who is muted by the authenticated user', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const mutedUser = await Factory.build('user', {
|
const mutedUser = await Factory.build('user', {
|
||||||
@ -440,7 +577,7 @@ und hinter tausend Stäben keine Welt.`,
|
|||||||
variables = { query: 'beitrag' }
|
variables = { query: 'beitrag' }
|
||||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
findResources: expect.not.arrayContaining([
|
searchResults: expect.not.arrayContaining([
|
||||||
{
|
{
|
||||||
__typename: 'Post',
|
__typename: 'Post',
|
||||||
id: 'muted-post',
|
id: 'muted-post',
|
||||||
@ -465,7 +602,7 @@ und hinter tausend Stäben keine Welt.`,
|
|||||||
variables = { query: 'myha' }
|
variables = { query: 'myha' }
|
||||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
findResources: [
|
searchResults: [
|
||||||
{
|
{
|
||||||
__typename: 'Tag',
|
__typename: 'Tag',
|
||||||
id: 'myHashtag',
|
id: 'myHashtag',
|
||||||
@ -477,6 +614,30 @@ und hinter tausend Stäben keine Welt.`,
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('searchPostQuery', () => {
|
||||||
|
describe('query with limit 1', () => {
|
||||||
|
it('has a count greater than 1', async () => {
|
||||||
|
variables = { query: 'beitrag', firstPosts: 1, postsOffset: 0 }
|
||||||
|
await expect(query({ query: searchPostQuery, variables })).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
searchPosts: {
|
||||||
|
postCount: 2,
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
__typename: 'Post',
|
||||||
|
id: 'a-post',
|
||||||
|
title: 'Beitrag',
|
||||||
|
content: 'Ein erster Beitrag',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -39,7 +39,8 @@ const matchBeginningOfWords = (str) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeWhitespace(str) {
|
export function normalizeWhitespace(str) {
|
||||||
return str.replace(/\s+/g, ' ').trim()
|
// delete the first character if it is !, @ or #
|
||||||
|
return str.replace(/^([!@#])/, '').replace(/\s+/g, ' ').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function escapeSpecialCharacters(str) {
|
export function escapeSpecialCharacters(str) {
|
||||||
|
|||||||
@ -265,9 +265,9 @@ describe('UpdateUser', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('supports updating location', async () => {
|
it('supports updating location', async () => {
|
||||||
variables = { ...variables, locationName: 'Hamburg, New Jersey, United States of America' }
|
variables = { ...variables, locationName: 'Hamburg, New Jersey, United States' }
|
||||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
|
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
|
||||||
data: { UpdateUser: { locationName: 'Hamburg, New Jersey, United States of America' } },
|
data: { UpdateUser: { locationName: 'Hamburg, New Jersey, United States' } },
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,5 +1,23 @@
|
|||||||
union SearchResult = Post | User | Tag
|
union SearchResult = Post | User | Tag
|
||||||
|
|
||||||
type Query {
|
type postSearchResults {
|
||||||
findResources(query: String!, limit: Int = 5): [SearchResult]!
|
postCount: Int
|
||||||
|
posts: [Post]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type userSearchResults {
|
||||||
|
userCount: Int
|
||||||
|
users: [User]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type hashtagSearchResults {
|
||||||
|
hashtagCount: Int
|
||||||
|
hashtags: [Tag]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
searchPosts(query: String!, firstPosts: Int, postsOffset: Int): postSearchResults!
|
||||||
|
searchUsers(query: String!, firstUsers: Int, usersOffset: Int): userSearchResults!
|
||||||
|
searchHashtags(query: String!, firstHashtags: Int, hashtagsOffset: Int): hashtagSearchResults!
|
||||||
|
searchResults(query: String!, limit: Int = 5): [SearchResult]!
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,7 +37,7 @@ Then("I should see the following posts in the select dropdown:", table => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Then("I should see the following users in the select dropdown:", table => {
|
Then("I should see the following users in the select dropdown:", table => {
|
||||||
cy.get(".ds-heading").should("contain", "Users");
|
cy.get(".search-heading").should("contain", "Users");
|
||||||
table.hashes().forEach(({ slug }) => {
|
table.hashes().forEach(({ slug }) => {
|
||||||
cy.get(".ds-select-dropdown").should("contain", slug);
|
cy.get(".ds-select-dropdown").should("contain", slug);
|
||||||
});
|
});
|
||||||
@ -85,6 +85,26 @@ Then(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Then("I should see the search results page", () => {
|
||||||
|
cy.location("pathname").should(
|
||||||
|
"eq",
|
||||||
|
"/search/search-results"
|
||||||
|
);
|
||||||
|
cy.location("search").should(
|
||||||
|
"eq",
|
||||||
|
"?search=PR"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Then("I should see the following posts on the search results page",
|
||||||
|
() => {
|
||||||
|
cy.get(".post-teaser").should(
|
||||||
|
"contain",
|
||||||
|
"101 Essays that will change the way you think"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
Then(
|
Then(
|
||||||
"I should not see posts without the searched-for term in the select dropdown",
|
"I should not see posts without the searched-for term in the select dropdown",
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ Feature: Search
|
|||||||
And we have the following posts in our database:
|
And we have the following posts in our database:
|
||||||
| id | title | content |
|
| id | title | content |
|
||||||
| p1 | 101 Essays that will change the way you think | 101 Essays, of course (PR)! |
|
| p1 | 101 Essays that will change the way you think | 101 Essays, of course (PR)! |
|
||||||
| p2 | No searched for content | will be found in this post, I guarantee |
|
| p2 | No content | will be found in this post, I guarantee |
|
||||||
And we have the following user accounts:
|
And we have the following user accounts:
|
||||||
| slug | name | id |
|
| slug | name | id |
|
||||||
| search-for-me | Search for me | user-for-search |
|
| search-for-me | Search for me | user-for-search |
|
||||||
@ -23,10 +23,10 @@ Feature: Search
|
|||||||
| title |
|
| title |
|
||||||
| 101 Essays that will change the way you think |
|
| 101 Essays that will change the way you think |
|
||||||
|
|
||||||
Scenario: Press enter starts search
|
Scenario: Press enter opens search page
|
||||||
When I type "PR" and press Enter
|
When I type "PR" and press Enter
|
||||||
Then I should have one item in the select dropdown
|
Then I should see the search results page
|
||||||
Then I should see the following posts in the select dropdown:
|
Then I should see the following posts on the search results page
|
||||||
| title |
|
| title |
|
||||||
| 101 Essays that will change the way you think |
|
| 101 Essays that will change the way you think |
|
||||||
|
|
||||||
|
|||||||
@ -16,13 +16,14 @@ h3,
|
|||||||
h4,
|
h4,
|
||||||
h5,
|
h5,
|
||||||
h6,
|
h6,
|
||||||
p {
|
p,
|
||||||
|
li {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul,
|
ol,
|
||||||
ol {
|
ul {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -251,7 +251,7 @@ $size-ribbon: 6px;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
$size-width-filter-sidebar: 85px;
|
$size-width-filter-sidebar: 85px;
|
||||||
$size-width-paginate: 100px;
|
$size-width-paginate: 200px;
|
||||||
$size-max-width-filter-menu: 1026px;
|
$size-max-width-filter-menu: 1026px;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -0,0 +1,234 @@
|
|||||||
|
import { config, mount } from '@vue/test-utils'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import SearchResults from './SearchResults'
|
||||||
|
import helpers from '~/storybook/helpers'
|
||||||
|
|
||||||
|
helpers.init()
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
localVue.directive('scrollTo', jest.fn())
|
||||||
|
|
||||||
|
config.stubs['client-only'] = '<span><slot /></span>'
|
||||||
|
config.stubs['nuxt-link'] = '<span><slot /></span>'
|
||||||
|
|
||||||
|
describe('SearchResults', () => {
|
||||||
|
let mocks, getters, propsData, wrapper
|
||||||
|
const Wrapper = () => {
|
||||||
|
const store = new Vuex.Store({
|
||||||
|
getters,
|
||||||
|
})
|
||||||
|
return mount(SearchResults, { mocks, localVue, propsData, store })
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks = {
|
||||||
|
$t: jest.fn(),
|
||||||
|
}
|
||||||
|
getters = {
|
||||||
|
'auth/user': () => {
|
||||||
|
return { id: 'u343', name: 'Matt' }
|
||||||
|
},
|
||||||
|
'auth/isModerator': () => false,
|
||||||
|
}
|
||||||
|
propsData = {
|
||||||
|
pageSize: 12,
|
||||||
|
}
|
||||||
|
// Wolle jest.useFakeTimers()
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Wolle jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
it('renders tab-navigation component', () => {
|
||||||
|
expect(wrapper.find('.tab-navigation').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('searchResults', () => {
|
||||||
|
describe('contains no results', () => {
|
||||||
|
it('renders hc-empty component', () => {
|
||||||
|
expect(wrapper.find('.hc-empty').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wolle beforeEach(jest.useFakeTimers)
|
||||||
|
|
||||||
|
describe('result contains 25 posts, 8 users and 0 hashtags', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
wrapper.setData({
|
||||||
|
posts: helpers.fakePost(12),
|
||||||
|
postCount: 25,
|
||||||
|
users: helpers.fakeUser(8),
|
||||||
|
userCount: 8,
|
||||||
|
activeTab: 'Post',
|
||||||
|
})
|
||||||
|
// Wolle await wrapper.vm.$nextTick()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows a total of 33 results', () => {
|
||||||
|
expect(wrapper.find('.total-search-results').text()).toContain('33')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows tab with 25 posts found', () => {
|
||||||
|
const postTab = wrapper.find('[data-test="Post-tab"]')
|
||||||
|
// console.log('postTab: ', postTab.html())
|
||||||
|
// console.log('wrapper: ', wrapper)
|
||||||
|
// Wolle jest.runAllTimers()
|
||||||
|
expect(postTab.text()).toContain('25')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows tab with 8 users found', () => {
|
||||||
|
const userTab = wrapper.find('[data-test="User-tab"]')
|
||||||
|
expect(userTab.text()).toContain('8')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows tab with 0 hashtags found', () => {
|
||||||
|
expect(wrapper.find('.Hashtag-tab').text()).toContain('0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has post tab as active tab', () => {
|
||||||
|
expect(wrapper.find('.Post-tab').classes('--active')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has user tab inactive', () => {
|
||||||
|
expect(wrapper.find('.User-tab').classes('--active')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has hashtag tab disabled', () => {
|
||||||
|
expect(wrapper.find('.Hashtag-tab').classes('--disabled')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays 12 (pageSize) posts', () => {
|
||||||
|
expect(wrapper.findAll('.post-teaser')).toHaveLength(12)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has post tab inactive after clicking on user tab', async () => {
|
||||||
|
wrapper.find('.User-tab').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
await expect(wrapper.find('.Post-tab').classes('--active')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has user tab active after clicking on user tab', async () => {
|
||||||
|
wrapper.find('.User-tab').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
await expect(wrapper.find('.User-tab').classes('--active')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays 8 users after clicking on user tab', async () => {
|
||||||
|
wrapper.find('.User-tab').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
await expect(wrapper.findAll('.user-teaser')).toHaveLength(8)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows the pagination buttons for posts', () => {
|
||||||
|
expect(wrapper.find('.pagination-buttons').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows no pagination buttons for users', async () => {
|
||||||
|
wrapper.find('.User-tab').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
await expect(wrapper.find('.pagination-buttons').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays page 1 of 3 for the 25 posts', () => {
|
||||||
|
expect(wrapper.find('.pagination-pageCount').text().replace(/\s+/g, ' ')).toContain(
|
||||||
|
'1 / 3',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays the next page button for the 25 posts', () => {
|
||||||
|
expect(wrapper.find('.next-button').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deactivates previous page button for the 25 posts', () => {
|
||||||
|
const previousButton = wrapper.find('[data-test="previous-button"]')
|
||||||
|
expect(previousButton.attributes().disabled).toEqual('disabled')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays page 2 / 3 when next-button is clicked', async () => {
|
||||||
|
wrapper.find('.next-button').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
await expect(wrapper.find('.pagination-pageCount').text().replace(/\s+/g, ' ')).toContain(
|
||||||
|
'2 / 3',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets apollo searchPosts offset to 12 when next-button is clicked', async () => {
|
||||||
|
wrapper.find('.next-button').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
await expect(
|
||||||
|
wrapper.vm.$options.apollo.searchPosts.variables.bind(wrapper.vm)(),
|
||||||
|
).toMatchObject({ query: undefined, firstPosts: 12, postsOffset: 12 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays the next page button when next-button is clicked', async () => {
|
||||||
|
wrapper.find('.next-button').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
await expect(wrapper.find('.next-button').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays the previous page button when next-button is clicked', async () => {
|
||||||
|
wrapper.find('.next-button').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
await expect(wrapper.find('.previous-button').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays page 3 / 3 when next-button is clicked twice', async () => {
|
||||||
|
wrapper.find('.next-button').trigger('click')
|
||||||
|
wrapper.find('.next-button').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
await expect(wrapper.find('.pagination-pageCount').text().replace(/\s+/g, ' ')).toContain(
|
||||||
|
'3 / 3',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets apollo searchPosts offset to 24 when next-button is clicked twice', async () => {
|
||||||
|
wrapper.find('.next-button').trigger('click')
|
||||||
|
wrapper.find('.next-button').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
await expect(
|
||||||
|
wrapper.vm.$options.apollo.searchPosts.variables.bind(wrapper.vm)(),
|
||||||
|
).toMatchObject({ query: undefined, firstPosts: 12, postsOffset: 24 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deactivates next page button when next-button is clicked twice', async () => {
|
||||||
|
const nextButton = wrapper.find('[data-test="next-button"]')
|
||||||
|
nextButton.trigger('click')
|
||||||
|
nextButton.trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(nextButton.attributes().disabled).toEqual('disabled')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays the previous page button when next-button is clicked twice', async () => {
|
||||||
|
wrapper.find('.next-button').trigger('click')
|
||||||
|
wrapper.find('.next-button').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
await expect(wrapper.find('.previous-button').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays page 1 / 3 when previous-button is clicked after next-button', async () => {
|
||||||
|
wrapper.find('.next-button').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.find('.previous-button').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
await expect(wrapper.find('.pagination-pageCount').text().replace(/\s+/g, ' ')).toContain(
|
||||||
|
'1 / 3',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets apollo searchPosts offset to 0 when previous-button is clicked after next-button', async () => {
|
||||||
|
wrapper.find('.next-button').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.find('.previous-button').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
await expect(
|
||||||
|
wrapper.vm.$options.apollo.searchPosts.variables.bind(wrapper.vm)(),
|
||||||
|
).toMatchObject({ query: undefined, firstPosts: 12, postsOffset: 0 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,148 @@
|
|||||||
|
import { storiesOf } from '@storybook/vue'
|
||||||
|
import { withA11y } from '@storybook/addon-a11y'
|
||||||
|
import SearchResults from './SearchResults'
|
||||||
|
import TabNavigation from '~/components/_new/generic/TabNavigation/TabNavigation'
|
||||||
|
import PostTeaser from '~/components/PostTeaser/PostTeaser'
|
||||||
|
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||||
|
import helpers from '~/storybook/helpers'
|
||||||
|
import faker from 'faker'
|
||||||
|
import { post } from '~/components/PostTeaser/PostTeaser.story.js'
|
||||||
|
import { user } from '~/components/UserTeaser/UserTeaser.story.js'
|
||||||
|
|
||||||
|
helpers.init()
|
||||||
|
|
||||||
|
const postMock = (fields) => {
|
||||||
|
return {
|
||||||
|
...post,
|
||||||
|
id: faker.random.uuid(),
|
||||||
|
createdAt: faker.date.past(),
|
||||||
|
updatedAt: faker.date.recent(),
|
||||||
|
deleted: false,
|
||||||
|
disabled: false,
|
||||||
|
typename: 'Post',
|
||||||
|
...fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMock = (fields) => {
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
id: faker.random.uuid(),
|
||||||
|
createdAt: faker.date.past(),
|
||||||
|
updatedAt: faker.date.recent(),
|
||||||
|
deleted: false,
|
||||||
|
disabled: false,
|
||||||
|
typename: 'User',
|
||||||
|
...fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const posts = [
|
||||||
|
postMock(),
|
||||||
|
postMock({ author: user }),
|
||||||
|
postMock({ title: faker.lorem.sentence() }),
|
||||||
|
postMock({ contentExcerpt: faker.lorem.paragraph() }),
|
||||||
|
postMock({ author: user }),
|
||||||
|
postMock({ title: faker.lorem.sentence() }),
|
||||||
|
postMock({ author: user }),
|
||||||
|
]
|
||||||
|
|
||||||
|
const users = [
|
||||||
|
userMock(),
|
||||||
|
userMock({ slug: 'louie-rider', name: 'Louie Rider' }),
|
||||||
|
userMock({ slug: 'louicinda-johnson', name: 'Louicinda Jonhson' }),
|
||||||
|
userMock({ slug: 'sam-louie', name: 'Sam Louie' }),
|
||||||
|
userMock({ slug: 'loucette', name: 'Loucette Rider' }),
|
||||||
|
userMock({ slug: 'louis', name: 'Louis' }),
|
||||||
|
userMock({ slug: 'louanna', name: 'Louanna' }),
|
||||||
|
]
|
||||||
|
|
||||||
|
storiesOf('SearchResults', module)
|
||||||
|
.addDecorator(withA11y)
|
||||||
|
.addDecorator(helpers.layout)
|
||||||
|
.add('given users', () => ({
|
||||||
|
components: { TabNavigation, PostTeaser, UserTeaser },
|
||||||
|
store: helpers.store,
|
||||||
|
data: () => ({
|
||||||
|
searchResults: users,
|
||||||
|
activeTab: 'users',
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
posts() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
users() {
|
||||||
|
return this.searchResults
|
||||||
|
},
|
||||||
|
activeResources() {
|
||||||
|
if (this.activeTab === 'posts') return this.posts
|
||||||
|
else if (this.activeTab === 'users') return this.users
|
||||||
|
},
|
||||||
|
tabOptions() {
|
||||||
|
return [
|
||||||
|
{ type: 'posts', title: `0 Posts` },
|
||||||
|
{ type: 'users', title: `${users.length} Users` },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: `<div class="search-results">
|
||||||
|
<tab-navigation
|
||||||
|
:tabs="tabOptions"
|
||||||
|
:activeTab="activeTab"
|
||||||
|
@switch-tab="tab => activeTab = tab"
|
||||||
|
/>
|
||||||
|
<section>
|
||||||
|
<ul v-if="activeResources.length">
|
||||||
|
<li v-for="resource in activeResources" :key="resource.key" class="list">
|
||||||
|
<post-teaser v-if="activeTab === 'posts'" :post="resource" />
|
||||||
|
<base-card v-else-if="activeTab === 'users'" :wideContent="true">
|
||||||
|
<user-teaser :user="resource" />
|
||||||
|
</base-card>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>`,
|
||||||
|
}))
|
||||||
|
.add('given posts', () => ({
|
||||||
|
components: { TabNavigation, PostTeaser, UserTeaser, SearchResults },
|
||||||
|
store: helpers.store,
|
||||||
|
data: () => ({
|
||||||
|
searchResults: posts,
|
||||||
|
activeTab: 'posts',
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
posts() {
|
||||||
|
return this.searchResults
|
||||||
|
},
|
||||||
|
users() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
activeResources() {
|
||||||
|
if (this.activeTab === 'posts') return this.posts
|
||||||
|
else if (this.activeTab === 'users') return this.users
|
||||||
|
},
|
||||||
|
tabOptions() {
|
||||||
|
return [
|
||||||
|
{ type: 'posts', title: `${posts.length} Posts` },
|
||||||
|
{ type: 'users', title: `0 Users` },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: `<div class="search-results">
|
||||||
|
<tab-navigation
|
||||||
|
:tabs="tabOptions"
|
||||||
|
:activeTab="activeTab"
|
||||||
|
@switch-tab="tab => activeTab = tab"
|
||||||
|
/>
|
||||||
|
<section>
|
||||||
|
<ul v-if="activeResources.length">
|
||||||
|
<li v-for="resource in activeResources" :key="resource.key" class="list">
|
||||||
|
<post-teaser v-if="activeTab === 'posts'" :post="resource" />
|
||||||
|
<base-card v-else-if="activeTab === 'users'" :wideContent="true">
|
||||||
|
<user-teaser :user="resource" />
|
||||||
|
</base-card>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>`,
|
||||||
|
}))
|
||||||
459
webapp/components/_new/features/SearchResults/SearchResults.vue
Normal file
459
webapp/components/_new/features/SearchResults/SearchResults.vue
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
<template>
|
||||||
|
<div id="search-results" class="search-results">
|
||||||
|
<ds-flex-item :width="{ base: '100%', sm: 3, md: 5, lg: 3 }">
|
||||||
|
<masonry-grid>
|
||||||
|
<!-- search text -->
|
||||||
|
<ds-grid-item class="grid-total-search-results" :row-span="1" column-span="fullWidth">
|
||||||
|
<ds-space margin-bottom="xxx-small" margin-top="xxx-small" centered>
|
||||||
|
<ds-text class="total-search-results">
|
||||||
|
{{ $t('search.for') }}
|
||||||
|
<strong>{{ '"' + (search || '') + '"' }}</strong>
|
||||||
|
</ds-text>
|
||||||
|
</ds-space>
|
||||||
|
</ds-grid-item>
|
||||||
|
|
||||||
|
<!-- tabs -->
|
||||||
|
<new-tab-navigation :tabs="tabOptions" :activeTab="activeTab" @switch-tab="switchTab" />
|
||||||
|
|
||||||
|
<!-- search results -->
|
||||||
|
|
||||||
|
<template v-if="!(!activeResourceCount || searchCount === 0)">
|
||||||
|
<!-- pagination buttons -->
|
||||||
|
<ds-grid-item v-if="activeResourceCount > pageSize" :row-span="2" column-span="fullWidth">
|
||||||
|
<ds-space centered>
|
||||||
|
<pagination-buttons
|
||||||
|
:hasNext="hasNext"
|
||||||
|
:showPageCounter="true"
|
||||||
|
:hasPrevious="hasPrevious"
|
||||||
|
:activePage="activePage"
|
||||||
|
:activeResourceCount="activeResourceCount"
|
||||||
|
:key="'Top'"
|
||||||
|
:pageSize="pageSize"
|
||||||
|
@back="previousResults"
|
||||||
|
@next="nextResults"
|
||||||
|
/>
|
||||||
|
</ds-space>
|
||||||
|
</ds-grid-item>
|
||||||
|
|
||||||
|
<!-- posts -->
|
||||||
|
<template v-if="activeTab === 'Post'">
|
||||||
|
<masonry-grid-item
|
||||||
|
v-for="post in activeResources"
|
||||||
|
:key="post.id"
|
||||||
|
:imageAspectRatio="post.image && post.image.aspectRatio"
|
||||||
|
>
|
||||||
|
<post-teaser
|
||||||
|
:post="post"
|
||||||
|
:width="{ base: '100%', md: '100%', xl: '50%' }"
|
||||||
|
@removePostFromList="posts = removePostFromList(post, posts)"
|
||||||
|
@pinPost="pinPost(post, refetchPostList)"
|
||||||
|
@unpinPost="unpinPost(post, refetchPostList)"
|
||||||
|
/>
|
||||||
|
</masonry-grid-item>
|
||||||
|
</template>
|
||||||
|
<!-- users -->
|
||||||
|
<template v-if="activeTab === 'User'">
|
||||||
|
<ds-grid-item v-for="user in activeResources" :key="user.id" :row-span="2">
|
||||||
|
<base-card :wideContent="true">
|
||||||
|
<user-teaser :user="user" />
|
||||||
|
</base-card>
|
||||||
|
</ds-grid-item>
|
||||||
|
</template>
|
||||||
|
<!-- hashtags -->
|
||||||
|
<template v-if="activeTab === 'Hashtag'">
|
||||||
|
<ds-grid-item v-for="hashtag in activeResources" :key="hashtag.id" :row-span="2">
|
||||||
|
<base-card :wideContent="true">
|
||||||
|
<hc-hashtag :id="hashtag.id" />
|
||||||
|
</base-card>
|
||||||
|
</ds-grid-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- pagination buttons -->
|
||||||
|
<ds-grid-item v-if="activeResourceCount > pageSize" :row-span="2" column-span="fullWidth">
|
||||||
|
<ds-space centered>
|
||||||
|
<pagination-buttons
|
||||||
|
:hasNext="hasNext"
|
||||||
|
:hasPrevious="hasPrevious"
|
||||||
|
:activePage="activePage"
|
||||||
|
:showPageCounter="true"
|
||||||
|
:activeResourceCount="activeResourceCount"
|
||||||
|
:key="'Bottom'"
|
||||||
|
:pageSize="pageSize"
|
||||||
|
:srollTo="'#search-results'"
|
||||||
|
@back="previousResults"
|
||||||
|
@next="nextResults"
|
||||||
|
/>
|
||||||
|
</ds-space>
|
||||||
|
</ds-grid-item>
|
||||||
|
</template>
|
||||||
|
<!-- Wolle <template v-else-if="$apollo.loading">
|
||||||
|
<ds-grid-item column-span="fullWidth">
|
||||||
|
<ds-space centered>
|
||||||
|
<ds-spinner size="base"></ds-spinner>
|
||||||
|
</ds-space>
|
||||||
|
</ds-grid-item>
|
||||||
|
</template> -->
|
||||||
|
<!-- Wolle double! -->
|
||||||
|
<!-- Wolle <template v-else>
|
||||||
|
<ds-grid-item column-span="fullWidth">
|
||||||
|
<hc-empty margin="xx-large" icon="file" />
|
||||||
|
</ds-grid-item>
|
||||||
|
</template> -->
|
||||||
|
<!-- no results -->
|
||||||
|
<ds-grid-item v-else :row-span="7" column-span="fullWidth">
|
||||||
|
<ds-space centered>
|
||||||
|
<hc-empty icon="tasks" :message="$t('search.no-results', { search })" />
|
||||||
|
</ds-space>
|
||||||
|
</ds-grid-item>
|
||||||
|
</masonry-grid>
|
||||||
|
</ds-flex-item>
|
||||||
|
|
||||||
|
<!-- Wolle old -->
|
||||||
|
<!-- <tab-navigation :tabs="tabOptions" :activeTab="activeTab" @switch-tab="switchTab" />
|
||||||
|
<section
|
||||||
|
:class="['results', activeTab === 'User' && '--user', !activeResourceCount > 0 && '--empty']"
|
||||||
|
>
|
||||||
|
<hc-empty
|
||||||
|
v-if="!activeResourceCount || searchCount === 0"
|
||||||
|
icon="tasks"
|
||||||
|
:message="$t('search.no-results', { search })"
|
||||||
|
/>
|
||||||
|
<template>
|
||||||
|
<pagination-buttons
|
||||||
|
v-if="activeResourceCount > pageSize"
|
||||||
|
:hasNext="hasNext"
|
||||||
|
:showPageCounter="true"
|
||||||
|
:hasPrevious="hasPrevious"
|
||||||
|
:activePage="activePage"
|
||||||
|
:activeResourceCount="activeResourceCount"
|
||||||
|
:key="'Top'"
|
||||||
|
:pageSize="pageSize"
|
||||||
|
@back="previousResults"
|
||||||
|
@next="nextResults"
|
||||||
|
/>
|
||||||
|
<masonry-grid v-if="activeTab === 'Post'">
|
||||||
|
<masonry-grid-item v-for="resource in activeResources" :key="resource.key">
|
||||||
|
<post-teaser :post="resource" />
|
||||||
|
</masonry-grid-item>
|
||||||
|
</masonry-grid>
|
||||||
|
|
||||||
|
<ul v-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>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul v-if="activeTab === 'Hashtag'" class="hashtag-list">
|
||||||
|
<li v-for="resource in activeResources" :key="resource.key" class="item">
|
||||||
|
<base-card :wideContent="true">
|
||||||
|
<hc-hashtag :id="resource.id" />
|
||||||
|
</base-card>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<pagination-buttons
|
||||||
|
v-if="activeResourceCount > pageSize"
|
||||||
|
:hasNext="hasNext"
|
||||||
|
:hasPrevious="hasPrevious"
|
||||||
|
:activePage="activePage"
|
||||||
|
:showPageCounter="true"
|
||||||
|
:activeResourceCount="activeResourceCount"
|
||||||
|
:key="'Bottom'"
|
||||||
|
:pageSize="pageSize"
|
||||||
|
:srollTo="'#search-results'"
|
||||||
|
@back="previousResults"
|
||||||
|
@next="nextResults"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</section> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import postListActions from '~/mixins/postListActions'
|
||||||
|
import { searchPosts, searchUsers, searchHashtags } 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'
|
||||||
|
// Wolle import TabNavigation from '~/components/_new/generic/TabNavigation/TabNavigation'
|
||||||
|
import NewTabNavigation from '~/components/_new/generic/TabNavigation/NewTabNavigation'
|
||||||
|
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||||
|
import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons'
|
||||||
|
import HcHashtag from '~/components/Hashtag/Hashtag'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
// Wolle TabNavigation,
|
||||||
|
NewTabNavigation,
|
||||||
|
HcEmpty,
|
||||||
|
MasonryGrid,
|
||||||
|
MasonryGridItem,
|
||||||
|
PostTeaser,
|
||||||
|
PaginationButtons,
|
||||||
|
UserTeaser,
|
||||||
|
HcHashtag,
|
||||||
|
},
|
||||||
|
mixins: [postListActions],
|
||||||
|
props: {
|
||||||
|
search: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
pageSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
posts: [],
|
||||||
|
users: [],
|
||||||
|
hashtags: [],
|
||||||
|
|
||||||
|
postCount: 0,
|
||||||
|
userCount: 0,
|
||||||
|
hashtagCount: 0,
|
||||||
|
|
||||||
|
postPage: 0,
|
||||||
|
userPage: 0,
|
||||||
|
hashtagPage: 0,
|
||||||
|
|
||||||
|
activeTab: null,
|
||||||
|
|
||||||
|
firstPosts: this.pageSize,
|
||||||
|
firstUsers: this.pageSize,
|
||||||
|
firstHashtags: this.pageSize,
|
||||||
|
|
||||||
|
postsOffset: 0,
|
||||||
|
usersOffset: 0,
|
||||||
|
hashtagsOffset: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
activeResources() {
|
||||||
|
if (this.activeTab === 'Post') return this.posts
|
||||||
|
if (this.activeTab === 'User') return this.users
|
||||||
|
if (this.activeTab === 'Hashtag') return this.hashtags
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
activeResourceCount() {
|
||||||
|
if (this.activeTab === 'Post') return this.postCount
|
||||||
|
if (this.activeTab === 'User') return this.userCount
|
||||||
|
if (this.activeTab === 'Hashtag') return this.hashtagCount
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
activePage() {
|
||||||
|
if (this.activeTab === 'Post') return this.postPage
|
||||||
|
if (this.activeTab === 'User') return this.userPage
|
||||||
|
if (this.activeTab === 'Hashtag') return this.hashtagPage
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
tabOptions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'Post',
|
||||||
|
title: this.$t('search.heading.Post', {}, this.postCount),
|
||||||
|
count: this.postCount,
|
||||||
|
disabled: this.postCount === 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'User',
|
||||||
|
title: this.$t('search.heading.User', {}, this.userCount),
|
||||||
|
count: this.userCount,
|
||||||
|
disabled: this.userCount === 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Hashtag',
|
||||||
|
title: this.$t('search.heading.Tag', {}, this.hashtagCount),
|
||||||
|
count: this.hashtagCount,
|
||||||
|
disabled: this.hashtagCount === 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
hasPrevious() {
|
||||||
|
if (this.activeTab === 'Post') return this.postsOffset > 0
|
||||||
|
if (this.activeTab === 'User') return this.usersOffset > 0
|
||||||
|
if (this.activeTab === 'Hashtag') return this.hashtagsOffset > 0
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
hasNext() {
|
||||||
|
if (this.activeTab === 'Post') return (this.postPage + 1) * this.pageSize < this.postCount
|
||||||
|
if (this.activeTab === 'User') return (this.userPage + 1) * this.pageSize < this.userCount
|
||||||
|
if (this.activeTab === 'Hashtag')
|
||||||
|
return (this.hashtagPage + 1) * this.pageSize < this.hashtagCount
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
searchCount() {
|
||||||
|
return this.postCount + this.userCount + this.hashtagCount
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clearPage() {
|
||||||
|
this.postPage = 0
|
||||||
|
this.userPage = 0
|
||||||
|
this.hashtagPage = 0
|
||||||
|
},
|
||||||
|
switchTab(tab) {
|
||||||
|
if (this.activeTab !== tab) {
|
||||||
|
this.activeTab = tab
|
||||||
|
}
|
||||||
|
},
|
||||||
|
previousResults() {
|
||||||
|
switch (this.activeTab) {
|
||||||
|
case 'Post':
|
||||||
|
this.postPage--
|
||||||
|
this.postsOffset = this.postPage * this.pageSize
|
||||||
|
break
|
||||||
|
case 'User':
|
||||||
|
this.userPage--
|
||||||
|
this.usersOffset = this.userPage * this.pageSize
|
||||||
|
break
|
||||||
|
case 'Hashtag':
|
||||||
|
this.hashtagPage--
|
||||||
|
this.hashtagsOffset = this.hashtagPage * this.pageSize
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nextResults() {
|
||||||
|
// scroll to top??
|
||||||
|
switch (this.activeTab) {
|
||||||
|
case 'Post':
|
||||||
|
this.postPage++
|
||||||
|
this.postsOffset += this.pageSize
|
||||||
|
break
|
||||||
|
case 'User':
|
||||||
|
this.userPage++
|
||||||
|
this.usersOffset += this.pageSize
|
||||||
|
break
|
||||||
|
case 'Hashtag':
|
||||||
|
this.hashtagPage++
|
||||||
|
this.hashtagsOffset += this.pageSize
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refetchPostList() {
|
||||||
|
this.$apollo.queries.searchPosts.refetch()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
searchHashtags: {
|
||||||
|
query() {
|
||||||
|
return searchHashtags
|
||||||
|
},
|
||||||
|
variables() {
|
||||||
|
const { firstHashtags, hashtagsOffset, search } = this
|
||||||
|
return {
|
||||||
|
query: search,
|
||||||
|
firstHashtags,
|
||||||
|
hashtagsOffset,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
skip() {
|
||||||
|
return !this.search
|
||||||
|
},
|
||||||
|
update({ searchHashtags }) {
|
||||||
|
this.hashtags = searchHashtags.hashtags
|
||||||
|
this.hashtagCount = searchHashtags.hashtagCount
|
||||||
|
if (this.postCount === 0 && this.userCount === 0 && this.hashtagCount > 0)
|
||||||
|
this.activeTab = 'Hashtag'
|
||||||
|
},
|
||||||
|
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.users
|
||||||
|
this.userCount = searchUsers.userCount
|
||||||
|
if (this.postCount === 0 && this.userCount > 0) this.activeTab = 'User'
|
||||||
|
},
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
},
|
||||||
|
searchPosts: {
|
||||||
|
query() {
|
||||||
|
return searchPosts
|
||||||
|
},
|
||||||
|
variables() {
|
||||||
|
const { firstPosts, postsOffset, search } = this
|
||||||
|
return {
|
||||||
|
query: search,
|
||||||
|
firstPosts,
|
||||||
|
postsOffset,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
skip() {
|
||||||
|
return !this.search
|
||||||
|
},
|
||||||
|
update({ searchPosts }) {
|
||||||
|
this.posts = searchPosts.posts
|
||||||
|
this.postCount = searchPosts.postCount
|
||||||
|
if (this.postCount > 0) this.activeTab = 'Post'
|
||||||
|
},
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
// Wolle check if still needed 👇🏼
|
||||||
|
.search-results {
|
||||||
|
> .results {
|
||||||
|
/* display: inline-block;*/
|
||||||
|
padding: $space-small;
|
||||||
|
background-color: $color-neutral-80;
|
||||||
|
border-radius: 0 $border-radius-base $border-radius-base $border-radius-base;
|
||||||
|
|
||||||
|
&.--user {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.--empty {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: $border-size-base solid $color-neutral-80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-list > .item {
|
||||||
|
transition: opacity 0.1s;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: $space-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wolle new
|
||||||
|
.grid-total-search-results {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
// margin-bottom: $space-x-small;
|
||||||
|
|
||||||
|
// > .base-button {
|
||||||
|
// display: block;
|
||||||
|
// width: 100%;
|
||||||
|
// margin-bottom: $space-x-small;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -5,62 +5,81 @@ import PaginationButtons from './PaginationButtons'
|
|||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
|
||||||
describe('PaginationButtons.vue', () => {
|
describe('PaginationButtons.vue', () => {
|
||||||
let propsData = {}
|
const propsData = {
|
||||||
|
showPageCounter: true,
|
||||||
|
activePage: 1,
|
||||||
|
activeResourceCount: 57,
|
||||||
|
}
|
||||||
let wrapper
|
let wrapper
|
||||||
let nextButton
|
const mocks = {
|
||||||
let backButton
|
$t: jest.fn(),
|
||||||
|
}
|
||||||
|
|
||||||
const Wrapper = () => {
|
const Wrapper = () => {
|
||||||
return mount(PaginationButtons, { propsData, localVue })
|
return mount(PaginationButtons, { mocks, propsData, localVue })
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('mount', () => {
|
describe('mount', () => {
|
||||||
describe('next button', () => {
|
beforeEach(() => {
|
||||||
beforeEach(() => {
|
wrapper = Wrapper()
|
||||||
propsData.hasNext = true
|
})
|
||||||
wrapper = Wrapper()
|
|
||||||
nextButton = wrapper.find('[data-test="next-button"]')
|
|
||||||
})
|
|
||||||
|
|
||||||
|
describe('next button', () => {
|
||||||
it('is disabled by default', () => {
|
it('is disabled by default', () => {
|
||||||
propsData = {}
|
const nextButton = wrapper.find('[data-test="next-button"]')
|
||||||
wrapper = Wrapper()
|
|
||||||
nextButton = wrapper.find('[data-test="next-button"]')
|
|
||||||
expect(nextButton.attributes().disabled).toEqual('disabled')
|
expect(nextButton.attributes().disabled).toEqual('disabled')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('is enabled if hasNext is true', () => {
|
it('is enabled if hasNext is true', async () => {
|
||||||
|
wrapper.setProps({ hasNext: true })
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
const nextButton = wrapper.find('[data-test="next-button"]')
|
||||||
expect(nextButton.attributes().disabled).toBeUndefined()
|
expect(nextButton.attributes().disabled).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits next when clicked', async () => {
|
it('emits next when clicked', async () => {
|
||||||
await nextButton.trigger('click')
|
wrapper.setProps({ hasNext: true })
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.find('[data-test="next-button"]').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
expect(wrapper.emitted().next).toHaveLength(1)
|
expect(wrapper.emitted().next).toHaveLength(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('back button', () => {
|
describe('previous button', () => {
|
||||||
beforeEach(() => {
|
|
||||||
propsData.hasPrevious = true
|
|
||||||
wrapper = Wrapper()
|
|
||||||
backButton = wrapper.find('[data-test="previous-button"]')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('is disabled by default', () => {
|
it('is disabled by default', () => {
|
||||||
propsData = {}
|
const previousButton = wrapper.find('[data-test="previous-button"]')
|
||||||
wrapper = Wrapper()
|
expect(previousButton.attributes().disabled).toEqual('disabled')
|
||||||
backButton = wrapper.find('[data-test="previous-button"]')
|
|
||||||
expect(backButton.attributes().disabled).toEqual('disabled')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('is enabled if hasPrevious is true', () => {
|
it('is enabled if hasPrevious is true', async () => {
|
||||||
expect(backButton.attributes().disabled).toBeUndefined()
|
wrapper.setProps({ hasPrevious: true })
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
const previousButton = wrapper.find('[data-test="previous-button"]')
|
||||||
|
expect(previousButton.attributes().disabled).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits back when clicked', async () => {
|
it('emits back when clicked', async () => {
|
||||||
await backButton.trigger('click')
|
wrapper.setProps({ hasPrevious: true })
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
wrapper.find('[data-test="previous-button"]').trigger('click')
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
expect(wrapper.emitted().back).toHaveLength(1)
|
expect(wrapper.emitted().back).toHaveLength(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('page counter', () => {
|
||||||
|
it('displays the page counter when showPageCount is true', () => {
|
||||||
|
const paginationPageCount = wrapper.find('[data-test="pagination-pageCount"]')
|
||||||
|
expect(paginationPageCount.text().replace(/\s+/g, ' ')).toEqual('2 / 3')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not display the page counter when showPageCount is false', async () => {
|
||||||
|
wrapper.setProps({ showPageCounter: false })
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
const paginationPageCount = wrapper.find('[data-test="pagination-pageCount"]')
|
||||||
|
expect(paginationPageCount.exists()).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,18 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="pagination-buttons">
|
<div class="pagination-buttons">
|
||||||
<base-button
|
<base-button
|
||||||
@click="$emit('back')"
|
class="previous-button"
|
||||||
:disabled="!hasPrevious"
|
:disabled="!hasPrevious"
|
||||||
icon="arrow-left"
|
icon="arrow-left"
|
||||||
circle
|
circle
|
||||||
data-test="previous-button"
|
data-test="previous-button"
|
||||||
|
@click="$emit('back')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<span v-if="showPageCounter" class="pagination-pageCount" data-test="pagination-pageCount">
|
||||||
|
{{ $t('search.page') }} {{ activePage + 1 }} /
|
||||||
|
{{ Math.floor((activeResourceCount - 1) / pageSize) + 1 }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<base-button
|
<base-button
|
||||||
@click="$emit('next')"
|
class="next-button"
|
||||||
:disabled="!hasNext"
|
:disabled="!hasNext"
|
||||||
icon="arrow-right"
|
icon="arrow-right"
|
||||||
circle
|
circle
|
||||||
data-test="next-button"
|
data-test="next-button"
|
||||||
|
@click="$emit('next')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -20,12 +28,31 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
pageSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 24,
|
||||||
|
},
|
||||||
hasNext: {
|
hasNext: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
hasPrevious: {
|
hasPrevious: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
},
|
||||||
|
activePage: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
totalResultCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
activeResourceCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
showPageCounter: {
|
||||||
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -39,4 +66,10 @@ export default {
|
|||||||
width: $size-width-paginate;
|
width: $size-width-paginate;
|
||||||
margin: $space-x-small auto;
|
margin: $space-x-small auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination-pageCount {
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
|
margin: 8px auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -0,0 +1,173 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Wolle siehe TabNavigator <ul class="tab-navigation">
|
||||||
|
<li
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.type"
|
||||||
|
:class="[
|
||||||
|
activeTab === tab.type && '--active',
|
||||||
|
tab.disabled && '--disabled',
|
||||||
|
'tab',
|
||||||
|
tab.type + '-tab',
|
||||||
|
]"
|
||||||
|
:style="tabWidth"
|
||||||
|
role="button"
|
||||||
|
:data-test="tab.type + '-tab'"
|
||||||
|
@click="$emit('switch-tab', tab.type)"
|
||||||
|
>
|
||||||
|
<ds-space margin="small">
|
||||||
|
{{ tab.count }}
|
||||||
|
</ds-space>
|
||||||
|
</li>
|
||||||
|
</ul> -->
|
||||||
|
<!-- Wolle <ds-grid-item class="profile-top-navigation" :row-span="3" column-span="fullWidth"> -->
|
||||||
|
<ds-grid-item class="profile-top-navigation" :row-span="tabs.length" column-span="fullWidth">
|
||||||
|
<base-card class="ds-tab-nav">
|
||||||
|
<ul class="Tabs">
|
||||||
|
<li
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.type"
|
||||||
|
:class="[
|
||||||
|
'Tabs__tab',
|
||||||
|
'pointer',
|
||||||
|
activeTab === tab.type && '--active',
|
||||||
|
tab.disabled && '--disabled',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<a @click="switchTab(tab)">
|
||||||
|
<ds-space margin="small">
|
||||||
|
<!-- Wolle translate -->
|
||||||
|
<!-- <client-only placeholder="Loading..."> -->
|
||||||
|
<client-only :placeholder="$t('client-only.loading')">
|
||||||
|
<ds-number :label="tab.title">
|
||||||
|
<hc-count-to slot="count" :end-val="tab.count" />
|
||||||
|
</ds-number>
|
||||||
|
</client-only>
|
||||||
|
</ds-space>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</base-card>
|
||||||
|
</ds-grid-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HcCountTo from '~/components/CountTo.vue'
|
||||||
|
// Wolle import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HcCountTo,
|
||||||
|
// Wolle MasonryGridItem,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
tabs: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
activeTab: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Wolle computed: {
|
||||||
|
// tabWidth() {
|
||||||
|
// return 'width: ' + String(100.0 / this.tabs.length) + '%'
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
methods: {
|
||||||
|
switchTab(tab) {
|
||||||
|
if (!tab.disabled) {
|
||||||
|
this.$emit('switch-tab', tab.type)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
// Wolle clean up
|
||||||
|
// .tab-navigation {
|
||||||
|
// display: flex;
|
||||||
|
// margin-top: $space-small;
|
||||||
|
|
||||||
|
// > .tab {
|
||||||
|
// font-weight: $font-weight-bold;
|
||||||
|
// padding: $space-x-small $space-small;
|
||||||
|
// margin-right: $space-xx-small;
|
||||||
|
// border-radius: $border-radius-base $border-radius-base 0 0;
|
||||||
|
// background: $color-neutral-100;
|
||||||
|
// cursor: pointer;
|
||||||
|
|
||||||
|
// &.--active {
|
||||||
|
// background: $color-neutral-80;
|
||||||
|
// border: none;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// &.--disabled {
|
||||||
|
// background: $background-color-disabled;
|
||||||
|
// border: $border-size-base solid $color-neutral-80;
|
||||||
|
// border-bottom: none;
|
||||||
|
// pointer-events: none;
|
||||||
|
// cursor: default;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// &:hover:not(.--active) {
|
||||||
|
// background: $color-neutral-85;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
.pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Tabs {
|
||||||
|
position: relative;
|
||||||
|
background-color: #fff;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
&__tab {
|
||||||
|
text-align: center;
|
||||||
|
height: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-bottom: 2px solid #c9c6ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.--active {
|
||||||
|
border-bottom: 2px solid #17b53f;
|
||||||
|
}
|
||||||
|
&.--disabled {
|
||||||
|
opacity: $opacity-disabled;
|
||||||
|
&:hover {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.profile-top-navigation {
|
||||||
|
position: sticky;
|
||||||
|
top: 53px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.ds-tab-nav.base-card {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.ds-tab-nav-item {
|
||||||
|
// Wolle is this doubled?
|
||||||
|
&.ds-tab-nav-item-active {
|
||||||
|
border-bottom: 3px solid #17b53f;
|
||||||
|
&:first-child {
|
||||||
|
border-bottom-left-radius: $border-radius-x-large;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-right-radius: $border-radius-x-large;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<ul class="tab-navigation">
|
||||||
|
<!-- Wolle Fix warning for 'switchTab', see below. -->
|
||||||
|
<li
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.type"
|
||||||
|
:class="[
|
||||||
|
activeTab === tab.type && '--active',
|
||||||
|
tab.disabled && '--disabled',
|
||||||
|
'tab',
|
||||||
|
tab.type + '-tab',
|
||||||
|
]"
|
||||||
|
:style="tabWidth"
|
||||||
|
role="button"
|
||||||
|
:data-test="tab.type + '-tab'"
|
||||||
|
@click="$emit('switch-tab', tab.type)"
|
||||||
|
>
|
||||||
|
<!-- Wolle <ds-space :class="lowerCase('Post-tab')" margin="small"> -->
|
||||||
|
<ds-space margin="small">
|
||||||
|
<!-- <ds-number :label="tab.title"> -->
|
||||||
|
<!-- Wolle <hc-count-to slot="count" :end-val="tab.count" /> -->
|
||||||
|
{{ tab.count }}
|
||||||
|
<!-- </ds-number> -->
|
||||||
|
<!-- <client-only placeholder="Loading...">
|
||||||
|
<ds-number :label="tab.title">
|
||||||
|
Wolle <hc-count-to slot="count" :end-val="tab.count" />
|
||||||
|
{{ tab.count }}
|
||||||
|
</ds-number>
|
||||||
|
</client-only> -->
|
||||||
|
</ds-space>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Wolle import HcCountTo from '~/components/CountTo.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// Wolle components: {
|
||||||
|
// HcCountTo,
|
||||||
|
// },
|
||||||
|
props: {
|
||||||
|
tabs: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
activeTab: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tabWidth() {
|
||||||
|
return 'width: ' + String(100.0 / this.tabs.length) + '%'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.tab-navigation {
|
||||||
|
display: flex;
|
||||||
|
margin-top: $space-small;
|
||||||
|
|
||||||
|
> .tab {
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
padding: $space-x-small $space-small;
|
||||||
|
margin-right: $space-xx-small;
|
||||||
|
border-radius: $border-radius-base $border-radius-base 0 0;
|
||||||
|
background: $color-neutral-100;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.--active {
|
||||||
|
background: $color-neutral-80;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.--disabled {
|
||||||
|
background: $background-color-disabled;
|
||||||
|
border: $border-size-base solid $color-neutral-80;
|
||||||
|
border-bottom: none;
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.--active) {
|
||||||
|
background: $color-neutral-85;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -9,7 +9,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { findResourcesQuery } from '~/graphql/Search.js'
|
import { searchQuery } from '~/graphql/Search.js'
|
||||||
import SearchableInput from '~/components/generic/SearchableInput/SearchableInput.vue'
|
import SearchableInput from '~/components/generic/SearchableInput/SearchableInput.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -28,14 +28,14 @@ export default {
|
|||||||
this.pending = true
|
this.pending = true
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: { findResources },
|
data: { searchResults },
|
||||||
} = await this.$apollo.query({
|
} = await this.$apollo.query({
|
||||||
query: findResourcesQuery,
|
query: searchQuery,
|
||||||
variables: {
|
variables: {
|
||||||
query: value,
|
query: value,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.searchResults = findResources
|
this.searchResults = searchResults
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.searchResults = []
|
this.searchResults = []
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-heading soft size="h5" class="search-heading">
|
<ds-heading soft size="h5" class="search-heading">
|
||||||
{{ $t(`search.heading.${resourceType}`) }}
|
{{ $t(`search.heading.${resourceType}`, {}, 2) }}
|
||||||
</ds-heading>
|
</ds-heading>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -60,13 +60,6 @@ describe('SearchableInput.vue', () => {
|
|||||||
expect(select.element.value).toBe('abcd')
|
expect(select.element.value).toBe('abcd')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('searches for the term when enter is pressed', async () => {
|
|
||||||
select.element.value = 'ab'
|
|
||||||
select.trigger('input')
|
|
||||||
select.trigger('keyup.enter')
|
|
||||||
await expect(wrapper.emitted().query[0]).toEqual(['ab'])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('calls onDelete when the delete key is pressed', () => {
|
it('calls onDelete when the delete key is pressed', () => {
|
||||||
const spy = jest.spyOn(wrapper.vm, 'onDelete')
|
const spy = jest.spyOn(wrapper.vm, 'onDelete')
|
||||||
select.trigger('input')
|
select.trigger('input')
|
||||||
@ -117,5 +110,15 @@ describe('SearchableInput.vue', () => {
|
|||||||
expect(mocks.$router.push).toHaveBeenCalledWith('?hashtag=Hashtag')
|
expect(mocks.$router.push).toHaveBeenCalledWith('?hashtag=Hashtag')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('opens the search result page when enter is pressed', async () => {
|
||||||
|
select.element.value = 'ab'
|
||||||
|
select.trigger('input')
|
||||||
|
select.trigger('keyup.enter')
|
||||||
|
expect(mocks.$router.push).toHaveBeenCalledWith({
|
||||||
|
path: '/search/search-results',
|
||||||
|
query: { search: 'ab' },
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -107,15 +107,12 @@ export default {
|
|||||||
this.$emit('query', this.value)
|
this.$emit('query', this.value)
|
||||||
}, this.delay)
|
}, this.delay)
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* TODO: on enter we should go to a dedicated search page!?
|
|
||||||
*/
|
|
||||||
onEnter(event) {
|
onEnter(event) {
|
||||||
clearTimeout(this.searchProcess)
|
this.$router.push({
|
||||||
if (!this.loading) {
|
path: '/search/search-results',
|
||||||
this.previousSearchTerm = this.unprocessedSearchInput
|
query: { search: this.unprocessedSearchInput },
|
||||||
this.$emit('query', this.unprocessedSearchInput)
|
})
|
||||||
}
|
this.$emit('clearSearch')
|
||||||
},
|
},
|
||||||
onDelete(event) {
|
onDelete(event) {
|
||||||
clearTimeout(this.searchProcess)
|
clearTimeout(this.searchProcess)
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import { userFragment, postFragment } from './Fragments'
|
import { userFragment, postFragment, tagsCategoriesAndPinnedFragment } from './Fragments'
|
||||||
|
|
||||||
export const findResourcesQuery = gql`
|
export const searchQuery = gql`
|
||||||
${userFragment}
|
${userFragment}
|
||||||
${postFragment}
|
${postFragment}
|
||||||
|
|
||||||
query($query: String!) {
|
query($query: String!) {
|
||||||
findResources(query: $query, limit: 5) {
|
searchResults(query: $query, limit: 5) {
|
||||||
__typename
|
__typename
|
||||||
... on Post {
|
... on Post {
|
||||||
...post
|
...post
|
||||||
@ -25,3 +25,51 @@ export const findResourcesQuery = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const searchPosts = gql`
|
||||||
|
${userFragment}
|
||||||
|
${postFragment}
|
||||||
|
${tagsCategoriesAndPinnedFragment}
|
||||||
|
|
||||||
|
query($query: String!, $firstPosts: Int, $postsOffset: Int) {
|
||||||
|
searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) {
|
||||||
|
postCount
|
||||||
|
posts {
|
||||||
|
__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) {
|
||||||
|
userCount
|
||||||
|
users {
|
||||||
|
__typename
|
||||||
|
...user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const searchHashtags = gql`
|
||||||
|
query($query: String!, $firstHashtags: Int, $hashtagsOffset: Int) {
|
||||||
|
searchHashtags(query: $query, firstHashtags: $firstHashtags, hashtagsOffset: $hashtagsOffset) {
|
||||||
|
hashtagCount
|
||||||
|
hashtags {
|
||||||
|
__typename
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|||||||
@ -75,6 +75,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"client-only": {
|
||||||
|
"loading": "Lade …"
|
||||||
|
},
|
||||||
"code-of-conduct": {
|
"code-of-conduct": {
|
||||||
"subheader": "für das Soziale Netzwerk von {ORGANIZATION_NAME}"
|
"subheader": "für das Soziale Netzwerk von {ORGANIZATION_NAME}"
|
||||||
},
|
},
|
||||||
@ -546,13 +549,18 @@
|
|||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"failed": "Nichts gefunden",
|
"failed": "Nichts gefunden",
|
||||||
|
"for": "Suche nach ",
|
||||||
"heading": {
|
"heading": {
|
||||||
"Post": "Beiträge",
|
"Post": "Beitrag ::: Beiträge",
|
||||||
"Tag": "Hashtags",
|
"Tag": "Hashtag ::: Hashtags",
|
||||||
"User": "Benutzer"
|
"User": "Benutzer ::: Benutzer"
|
||||||
},
|
},
|
||||||
"hint": "Wonach suchst Du?",
|
"hint": "Wonach suchst Du?",
|
||||||
"placeholder": "Suchen"
|
"no-results": "Keine Ergebnisse für \"{search}\" gefunden. Versuch' es mit einem anderen Begriff!",
|
||||||
|
"page": "Seite",
|
||||||
|
"placeholder": "Suchen",
|
||||||
|
"results": "Ergebnis gefunden ::: Ergebnisse gefunden",
|
||||||
|
"title": "Suchergebnisse"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"blocked-users": {
|
"blocked-users": {
|
||||||
|
|||||||
@ -75,6 +75,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"client-only": {
|
||||||
|
"loading": "Loading …"
|
||||||
|
},
|
||||||
"code-of-conduct": {
|
"code-of-conduct": {
|
||||||
"subheader": "for the social network of {ORGANIZATION_NAME}"
|
"subheader": "for the social network of {ORGANIZATION_NAME}"
|
||||||
},
|
},
|
||||||
@ -546,13 +549,18 @@
|
|||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"failed": "Nothing found",
|
"failed": "Nothing found",
|
||||||
|
"for": "Searching for ",
|
||||||
"heading": {
|
"heading": {
|
||||||
"Post": "Posts",
|
"Post": "Post ::: Posts",
|
||||||
"Tag": "Hashtags",
|
"Tag": "Hashtag ::: Hashtags",
|
||||||
"User": "Users"
|
"User": "User ::: Users"
|
||||||
},
|
},
|
||||||
"hint": "What are you searching for?",
|
"hint": "What are you searching for?",
|
||||||
"placeholder": "Search"
|
"no-results": "No results found for \"{search}\". Try a different search term!",
|
||||||
|
"page": "Page",
|
||||||
|
"placeholder": "Search",
|
||||||
|
"results": "result found ::: results found",
|
||||||
|
"title": "Search Results"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"blocked-users": {
|
"blocked-users": {
|
||||||
|
|||||||
39
webapp/mixins/postListActions.js
Normal file
39
webapp/mixins/postListActions.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import PostMutations from '~/graphql/PostMutations'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
removePostFromList(deletedPost, posts) {
|
||||||
|
return posts.filter((post) => {
|
||||||
|
return post.id !== deletedPost.id
|
||||||
|
})
|
||||||
|
},
|
||||||
|
pinPost(post, refetchPostList = () => {}) {
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: PostMutations().pinPost,
|
||||||
|
variables: {
|
||||||
|
id: post.id
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success(this.$t('post.menu.pinnedSuccessfully'))
|
||||||
|
refetchPostList()
|
||||||
|
})
|
||||||
|
.catch((error) => this.$toast.error(error.message))
|
||||||
|
},
|
||||||
|
unpinPost(post, refetchPostList = () => {}) {
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: PostMutations().unpinPost,
|
||||||
|
variables: {
|
||||||
|
id: post.id
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success(this.$t('post.menu.unpinnedSuccessfully'))
|
||||||
|
refetchPostList()
|
||||||
|
})
|
||||||
|
.catch((error) => this.$toast.error(error.message))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
23910
webapp/package-lock.json
generated
Normal file
23910
webapp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -28,9 +28,9 @@
|
|||||||
>
|
>
|
||||||
<post-teaser
|
<post-teaser
|
||||||
:post="post"
|
:post="post"
|
||||||
@removePostFromList="deletePost"
|
@removePostFromList="posts = removePostFromList(post, posts)"
|
||||||
@pinPost="pinPost"
|
@pinPost="pinPost(post, refetchPostList)"
|
||||||
@unpinPost="unpinPost"
|
@unpinPost="unpinPost(post, refetchPostList)"
|
||||||
/>
|
/>
|
||||||
</masonry-grid-item>
|
</masonry-grid-item>
|
||||||
</template>
|
</template>
|
||||||
@ -64,6 +64,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import postListActions from '~/mixins/postListActions'
|
||||||
// import DonationInfo from '~/components/DonationInfo/DonationInfo.vue'
|
// import DonationInfo from '~/components/DonationInfo/DonationInfo.vue'
|
||||||
import HashtagsFilter from '~/components/HashtagsFilter/HashtagsFilter.vue'
|
import HashtagsFilter from '~/components/HashtagsFilter/HashtagsFilter.vue'
|
||||||
import HcEmpty from '~/components/Empty/Empty'
|
import HcEmpty from '~/components/Empty/Empty'
|
||||||
@ -72,7 +73,6 @@ import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
|
|||||||
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
||||||
import { mapGetters, mapMutations } from 'vuex'
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
import { filterPosts } from '~/graphql/PostQuery.js'
|
import { filterPosts } from '~/graphql/PostQuery.js'
|
||||||
import PostMutations from '~/graphql/PostMutations'
|
|
||||||
import UpdateQuery from '~/components/utils/UpdateQuery'
|
import UpdateQuery from '~/components/utils/UpdateQuery'
|
||||||
import links from '~/constants/links.js'
|
import links from '~/constants/links.js'
|
||||||
|
|
||||||
@ -85,6 +85,7 @@ export default {
|
|||||||
MasonryGrid,
|
MasonryGrid,
|
||||||
MasonryGridItem,
|
MasonryGridItem,
|
||||||
},
|
},
|
||||||
|
mixins: [postListActions],
|
||||||
data() {
|
data() {
|
||||||
const { hashtag = null } = this.$route.query
|
const { hashtag = null } = this.$route.query
|
||||||
return {
|
return {
|
||||||
@ -162,41 +163,14 @@ export default {
|
|||||||
updateQuery: UpdateQuery(this, { $state, pageKey: 'Post' }),
|
updateQuery: UpdateQuery(this, { $state, pageKey: 'Post' }),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deletePost(deletedPost) {
|
|
||||||
this.posts = this.posts.filter((post) => {
|
|
||||||
return post.id !== deletedPost.id
|
|
||||||
})
|
|
||||||
},
|
|
||||||
resetPostList() {
|
resetPostList() {
|
||||||
this.offset = 0
|
this.offset = 0
|
||||||
this.posts = []
|
this.posts = []
|
||||||
this.hasMore = true
|
this.hasMore = true
|
||||||
},
|
},
|
||||||
pinPost(post) {
|
refetchPostList() {
|
||||||
this.$apollo
|
this.resetPostList()
|
||||||
.mutate({
|
this.$apollo.queries.Post.refetch()
|
||||||
mutation: PostMutations().pinPost,
|
|
||||||
variables: { id: post.id },
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.$toast.success(this.$t('post.menu.pinnedSuccessfully'))
|
|
||||||
this.resetPostList()
|
|
||||||
this.$apollo.queries.Post.refetch()
|
|
||||||
})
|
|
||||||
.catch((error) => this.$toast.error(error.message))
|
|
||||||
},
|
|
||||||
unpinPost(post) {
|
|
||||||
this.$apollo
|
|
||||||
.mutate({
|
|
||||||
mutation: PostMutations().unpinPost,
|
|
||||||
variables: { id: post.id },
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.$toast.success(this.$t('post.menu.unpinnedSuccessfully'))
|
|
||||||
this.resetPostList()
|
|
||||||
this.$apollo.queries.Post.refetch()
|
|
||||||
})
|
|
||||||
.catch((error) => this.$toast.error(error.message))
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
|
|||||||
@ -90,7 +90,7 @@ describe('PostIndex', () => {
|
|||||||
expect(wrapper.vm.selected).toEqual(propsData.filterOptions[1].label)
|
expect(wrapper.vm.selected).toEqual(propsData.filterOptions[1].label)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('refreshes the notificaitons', () => {
|
it('refreshes the notifications', () => {
|
||||||
expect(mocks.$apollo.queries.notifications.refresh).toHaveBeenCalledTimes(1)
|
expect(mocks.$apollo.queries.notifications.refresh).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -27,7 +27,9 @@
|
|||||||
<post-teaser
|
<post-teaser
|
||||||
:post="relatedPost"
|
:post="relatedPost"
|
||||||
:width="{ base: '100%', lg: 1 }"
|
:width="{ base: '100%', lg: 1 }"
|
||||||
@removePostFromList="removePostFromList"
|
@removePostFromList="post.relatedContributions = removePostFromList(relatedPost, post.relatedContributions)"
|
||||||
|
@pinPost="pinPost(relatedPost, refetchPostList)"
|
||||||
|
@unpinPost="unpinPost(relatedPost, refetchPostList)"
|
||||||
/>
|
/>
|
||||||
</masonry-grid-item>
|
</masonry-grid-item>
|
||||||
</masonry-grid>
|
</masonry-grid>
|
||||||
@ -37,6 +39,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import postListActions from '~/mixins/postListActions'
|
||||||
import HcEmpty from '~/components/Empty/Empty'
|
import HcEmpty from '~/components/Empty/Empty'
|
||||||
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
|
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
|
||||||
import HcCategory from '~/components/Category'
|
import HcCategory from '~/components/Category'
|
||||||
@ -47,10 +50,6 @@ import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
|||||||
import { sortTagsAlphabetically } from '~/components/utils/PostHelpers'
|
import { sortTagsAlphabetically } from '~/components/utils/PostHelpers'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
transition: {
|
|
||||||
name: 'slide-up',
|
|
||||||
mode: 'out-in',
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
PostTeaser,
|
PostTeaser,
|
||||||
HcCategory,
|
HcCategory,
|
||||||
@ -59,6 +58,11 @@ export default {
|
|||||||
MasonryGrid,
|
MasonryGrid,
|
||||||
MasonryGridItem,
|
MasonryGridItem,
|
||||||
},
|
},
|
||||||
|
transition: {
|
||||||
|
name: 'slide-up',
|
||||||
|
mode: 'out-in',
|
||||||
|
},
|
||||||
|
mixins: [postListActions],
|
||||||
computed: {
|
computed: {
|
||||||
post() {
|
post() {
|
||||||
return this.Post ? this.Post[0] || {} : {}
|
return this.Post ? this.Post[0] || {} : {}
|
||||||
@ -68,10 +72,8 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removePostFromList(deletedPost) {
|
refetchPostList() {
|
||||||
this.post.relatedContributions = this.post.relatedContributions.filter((contribution) => {
|
this.$apollo.queries.Post.refetch()
|
||||||
return contribution.id !== deletedPost.id
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
|
|||||||
@ -108,7 +108,9 @@
|
|||||||
|
|
||||||
<ds-flex-item :width="{ base: '100%', sm: 3, md: 5, lg: 3 }">
|
<ds-flex-item :width="{ base: '100%', sm: 3, md: 5, lg: 3 }">
|
||||||
<masonry-grid>
|
<masonry-grid>
|
||||||
<ds-grid-item class="profile-top-navigation" :row-span="3" column-span="fullWidth">
|
<!-- TapNavigation -->
|
||||||
|
<new-tab-navigation :tabs="tabOptions" :activeTab="tabActive" @switch-tab="handleTab" />
|
||||||
|
<!-- Wolle <ds-grid-item class="profile-top-navigation" :row-span="3" column-span="fullWidth">
|
||||||
<base-card class="ds-tab-nav">
|
<base-card class="ds-tab-nav">
|
||||||
<ul class="Tabs">
|
<ul class="Tabs">
|
||||||
<li class="Tabs__tab pointer" :class="{ active: tabActive === 'post' }">
|
<li class="Tabs__tab pointer" :class="{ active: tabActive === 'post' }">
|
||||||
@ -150,8 +152,9 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</base-card>
|
</base-card>
|
||||||
</ds-grid-item>
|
</ds-grid-item> -->
|
||||||
|
|
||||||
|
<!-- feed -->
|
||||||
<ds-grid-item :row-span="2" column-span="fullWidth">
|
<ds-grid-item :row-span="2" column-span="fullWidth">
|
||||||
<ds-space centered>
|
<ds-space centered>
|
||||||
<nuxt-link :to="{ name: 'post-create' }">
|
<nuxt-link :to="{ name: 'post-create' }">
|
||||||
@ -181,9 +184,9 @@
|
|||||||
<post-teaser
|
<post-teaser
|
||||||
:post="post"
|
:post="post"
|
||||||
:width="{ base: '100%', md: '100%', xl: '50%' }"
|
:width="{ base: '100%', md: '100%', xl: '50%' }"
|
||||||
@removePostFromList="removePostFromList"
|
@removePostFromList="posts = removePostFromList(post, posts)"
|
||||||
@pinPost="pinPost"
|
@pinPost="pinPost(post, refetchPostList)"
|
||||||
@unpinPost="unpinPost"
|
@unpinPost="unpinPost(post, refetchPostList)"
|
||||||
/>
|
/>
|
||||||
</masonry-grid-item>
|
</masonry-grid-item>
|
||||||
</template>
|
</template>
|
||||||
@ -210,6 +213,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import uniqBy from 'lodash/uniqBy'
|
import uniqBy from 'lodash/uniqBy'
|
||||||
|
import postListActions from '~/mixins/postListActions'
|
||||||
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
|
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
|
||||||
import HcFollowButton from '~/components/FollowButton.vue'
|
import HcFollowButton from '~/components/FollowButton.vue'
|
||||||
import HcCountTo from '~/components/CountTo.vue'
|
import HcCountTo from '~/components/CountTo.vue'
|
||||||
@ -221,6 +225,7 @@ import HcUpload from '~/components/Upload'
|
|||||||
import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
|
import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
|
||||||
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
|
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
|
||||||
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
||||||
|
import NewTabNavigation from '~/components/_new/generic/TabNavigation/NewTabNavigation'
|
||||||
import { profilePagePosts } from '~/graphql/PostQuery'
|
import { profilePagePosts } from '~/graphql/PostQuery'
|
||||||
import UserQuery from '~/graphql/User'
|
import UserQuery from '~/graphql/User'
|
||||||
import { muteUser, unmuteUser } from '~/graphql/settings/MutedUsers'
|
import { muteUser, unmuteUser } from '~/graphql/settings/MutedUsers'
|
||||||
@ -251,7 +256,9 @@ export default {
|
|||||||
MasonryGrid,
|
MasonryGrid,
|
||||||
MasonryGridItem,
|
MasonryGridItem,
|
||||||
FollowList,
|
FollowList,
|
||||||
|
NewTabNavigation,
|
||||||
},
|
},
|
||||||
|
mixins: [postListActions],
|
||||||
transition: {
|
transition: {
|
||||||
name: 'slide-up',
|
name: 'slide-up',
|
||||||
mode: 'out-in',
|
mode: 'out-in',
|
||||||
@ -286,17 +293,36 @@ export default {
|
|||||||
const { slug } = this.user || {}
|
const { slug } = this.user || {}
|
||||||
return slug && `@${slug}`
|
return slug && `@${slug}`
|
||||||
},
|
},
|
||||||
|
tabOptions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'post',
|
||||||
|
title: this.$t('common.post', null, this.user.contributionsCount),
|
||||||
|
count: this.user.contributionsCount,
|
||||||
|
disabled: this.user.contributionsCount === 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'comment',
|
||||||
|
title: this.$t('profile.commented'),
|
||||||
|
count: this.user.commentedCount,
|
||||||
|
disabled: this.user.commentedCount === 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'shout',
|
||||||
|
title: this.$t('profile.shouted'),
|
||||||
|
count: this.user.shoutedCount,
|
||||||
|
disabled: this.user.shoutedCount === 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removePostFromList(deletedPost) {
|
|
||||||
this.posts = this.posts.filter((post) => {
|
|
||||||
return post.id !== deletedPost.id
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleTab(tab) {
|
handleTab(tab) {
|
||||||
this.tabActive = tab
|
if (this.tabActive !== tab) {
|
||||||
this.filter = tabToFilterMapping({ tab, id: this.$route.params.id })
|
this.tabActive = tab
|
||||||
this.resetPostList()
|
this.filter = tabToFilterMapping({ tab, id: this.$route.params.id })
|
||||||
|
this.resetPostList()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
uniq(items, field = 'id') {
|
uniq(items, field = 'id') {
|
||||||
return uniqBy(items, field)
|
return uniqBy(items, field)
|
||||||
@ -321,6 +347,10 @@ export default {
|
|||||||
this.posts = []
|
this.posts = []
|
||||||
this.hasMore = true
|
this.hasMore = true
|
||||||
},
|
},
|
||||||
|
refetchPostList() {
|
||||||
|
this.resetPostList()
|
||||||
|
this.$apollo.queries.profilePagePosts.refetch()
|
||||||
|
},
|
||||||
async muteUser(user) {
|
async muteUser(user) {
|
||||||
try {
|
try {
|
||||||
await this.$apollo.mutate({ mutation: muteUser(), variables: { id: user.id } })
|
await this.$apollo.mutate({ mutation: muteUser(), variables: { id: user.id } })
|
||||||
@ -361,40 +391,6 @@ export default {
|
|||||||
this.$apollo.queries.User.refetch()
|
this.$apollo.queries.User.refetch()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async deleteUser(userdata) {
|
|
||||||
this.$store.commit('modal/SET_OPEN', {
|
|
||||||
name: 'delete',
|
|
||||||
data: {
|
|
||||||
userdata: userdata,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
pinPost(post) {
|
|
||||||
this.$apollo
|
|
||||||
.mutate({
|
|
||||||
mutation: PostMutations().pinPost,
|
|
||||||
variables: { id: post.id },
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.$toast.success(this.$t('post.menu.pinnedSuccessfully'))
|
|
||||||
this.resetPostList()
|
|
||||||
this.$apollo.queries.profilePagePosts.refetch()
|
|
||||||
})
|
|
||||||
.catch((error) => this.$toast.error(error.message))
|
|
||||||
},
|
|
||||||
unpinPost(post) {
|
|
||||||
this.$apollo
|
|
||||||
.mutate({
|
|
||||||
mutation: PostMutations().unpinPost,
|
|
||||||
variables: { id: post.id },
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.$toast.success(this.$t('post.menu.unpinnedSuccessfully'))
|
|
||||||
this.resetPostList()
|
|
||||||
this.$apollo.queries.profilePagePosts.refetch()
|
|
||||||
})
|
|
||||||
.catch((error) => this.$toast.error(error.message))
|
|
||||||
},
|
|
||||||
optimisticFollow({ followedByCurrentUser }) {
|
optimisticFollow({ followedByCurrentUser }) {
|
||||||
/*
|
/*
|
||||||
* Note: followedByCountStartValue is updated to avoid counting from 0 when follow/unfollow
|
* Note: followedByCountStartValue is updated to avoid counting from 0 when follow/unfollow
|
||||||
@ -457,33 +453,33 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.pointer {
|
// Wolle .pointer {
|
||||||
cursor: pointer;
|
// cursor: pointer;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.Tabs {
|
// Wolle .Tabs {
|
||||||
position: relative;
|
// position: relative;
|
||||||
background-color: #fff;
|
// background-color: #fff;
|
||||||
height: 100%;
|
// height: 100%;
|
||||||
display: flex;
|
// display: flex;
|
||||||
margin: 0;
|
// margin: 0;
|
||||||
padding: 0;
|
// padding: 0;
|
||||||
list-style: none;
|
// list-style: none;
|
||||||
|
|
||||||
&__tab {
|
// &__tab {
|
||||||
text-align: center;
|
// text-align: center;
|
||||||
height: 100%;
|
// height: 100%;
|
||||||
flex-grow: 1;
|
// flex-grow: 1;
|
||||||
|
|
||||||
&:hover {
|
// &:hover {
|
||||||
border-bottom: 2px solid #c9c6ce;
|
// border-bottom: 2px solid #c9c6ce;
|
||||||
}
|
// }
|
||||||
|
|
||||||
&.active {
|
// &.active {
|
||||||
border-bottom: 2px solid #17b53f;
|
// border-bottom: 2px solid #17b53f;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
.profile-avatar.user-avatar {
|
.profile-avatar.user-avatar {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
margin-top: -60px;
|
margin-top: -60px;
|
||||||
@ -495,26 +491,26 @@ export default {
|
|||||||
right: $space-x-small;
|
right: $space-x-small;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.profile-top-navigation {
|
// Wolle .profile-top-navigation {
|
||||||
position: sticky;
|
// position: sticky;
|
||||||
top: 53px;
|
// top: 53px;
|
||||||
z-index: 2;
|
// z-index: 2;
|
||||||
}
|
// }
|
||||||
.ds-tab-nav.base-card {
|
// Wolle .ds-tab-nav.base-card {
|
||||||
padding: 0;
|
// padding: 0;
|
||||||
|
|
||||||
.ds-tab-nav-item {
|
// .ds-tab-nav-item {
|
||||||
&.ds-tab-nav-item-active {
|
// &.ds-tab-nav-item-active {
|
||||||
border-bottom: 3px solid #17b53f;
|
// border-bottom: 3px solid #17b53f;
|
||||||
&:first-child {
|
// &:first-child {
|
||||||
border-bottom-left-radius: $border-radius-x-large;
|
// border-bottom-left-radius: $border-radius-x-large;
|
||||||
}
|
// }
|
||||||
&:last-child {
|
// &:last-child {
|
||||||
border-bottom-right-radius: $border-radius-x-large;
|
// border-bottom-right-radius: $border-radius-x-large;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
.profile-post-add-button {
|
.profile-post-add-button {
|
||||||
box-shadow: $box-shadow-x-large;
|
box-shadow: $box-shadow-x-large;
|
||||||
}
|
}
|
||||||
|
|||||||
24
webapp/pages/search/search-results.vue
Normal file
24
webapp/pages/search/search-results.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<search-results :search="search" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SearchResults from '~/components/_new/features/SearchResults/SearchResults'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
layout: 'default',
|
||||||
|
watchQuery: ['search'],
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t('search.title'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
SearchResults,
|
||||||
|
},
|
||||||
|
asyncData(context) {
|
||||||
|
const { search = null } = context.query
|
||||||
|
return { search }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
17
webapp/store/search.js
Normal file
17
webapp/store/search.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export const state = () => {
|
||||||
|
return {
|
||||||
|
searchValue: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
SET_VALUE(state, ctx) {
|
||||||
|
state.searchValue = ctx.searchValue || ''
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
searchValue(state) {
|
||||||
|
return state.searchValue
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -70,6 +70,20 @@ const helpers = {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
fakePost(n) {
|
||||||
|
return new Array(n || 1).fill(0).map(() => {
|
||||||
|
const title = faker.lorem.words()
|
||||||
|
const content = faker.lorem.paragraph()
|
||||||
|
return {
|
||||||
|
id: faker.random.uuid(),
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
slug: faker.lorem.slug(title),
|
||||||
|
shoutedCount: faker.random.number(),
|
||||||
|
commentsCount: faker.random.number(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default helpers
|
export default helpers
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user