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(
|
||||
[...Array(30).keys()].map(() => Factory.build('user')),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
additionalUsers.map(async (user) => {
|
||||
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(
|
||||
[...Array(30).keys()].map(() =>
|
||||
Factory.build(
|
||||
|
||||
@ -86,7 +86,10 @@ export default shield(
|
||||
'*': deny,
|
||||
findPosts: allow,
|
||||
findUsers: allow,
|
||||
findResources: allow,
|
||||
searchResults: allow,
|
||||
searchPosts: allow,
|
||||
searchUsers: allow,
|
||||
searchHashtags: allow,
|
||||
embed: allow,
|
||||
Category: 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
|
||||
|
||||
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 {
|
||||
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 { id: thisUserId } = context.user
|
||||
const { id: userId } = context.user
|
||||
|
||||
const postCypher = `
|
||||
CALL db.index.fulltext.queryNodes('post_fulltext_search', $query)
|
||||
YIELD node as resource, score
|
||||
MATCH (resource)<-[:WROTE]-(author:User)
|
||||
WHERE score >= 0.0
|
||||
AND NOT (
|
||||
author.deleted = true OR author.disabled = true
|
||||
OR resource.deleted = true OR resource.disabled = true
|
||||
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))
|
||||
const searchType = query.replace(/^([!@#]?).*$/, '$1')
|
||||
const searchString = query.replace(/^([!@#])/, '')
|
||||
|
||||
const params = {
|
||||
query: queryString(searchString),
|
||||
skip: 0,
|
||||
limit,
|
||||
userId,
|
||||
}
|
||||
LIMIT $limit
|
||||
`
|
||||
|
||||
const userCypher = `
|
||||
CALL db.index.fulltext.queryNodes('user_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 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
|
||||
`
|
||||
if (searchType === '')
|
||||
return [
|
||||
...(await getSearchResults(context, searchPostsSetup, params)),
|
||||
...(await getSearchResults(context, searchUsersSetup, params)),
|
||||
...(await getSearchResults(context, searchHashtagsSetup, params)),
|
||||
]
|
||||
|
||||
const myQuery = queryString(query)
|
||||
|
||||
const session = context.driver.session()
|
||||
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()
|
||||
}
|
||||
params.limit = 15
|
||||
const type = multiSearchMap.find((obj) => obj.symbol === searchType)
|
||||
return getSearchResults(context, type.setup, params)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ afterAll(async () => {
|
||||
|
||||
const searchQuery = gql`
|
||||
query($query: String!) {
|
||||
findResources(query: $query, limit: 5) {
|
||||
searchResults(query: $query, limit: 5) {
|
||||
__typename
|
||||
... on Post {
|
||||
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', () => {
|
||||
let variables
|
||||
|
||||
@ -65,7 +80,7 @@ describe('resolvers/searches', () => {
|
||||
variables = { query: 'John' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
searchResults: [
|
||||
{
|
||||
id: 'a-user',
|
||||
name: 'John Doe',
|
||||
@ -95,7 +110,7 @@ describe('resolvers/searches', () => {
|
||||
variables = { query: 'beitrag' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
searchResults: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'a-post',
|
||||
@ -114,7 +129,7 @@ describe('resolvers/searches', () => {
|
||||
variables = { query: 'BEITRAG' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
searchResults: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'a-post',
|
||||
@ -132,7 +147,7 @@ describe('resolvers/searches', () => {
|
||||
it('returns empty search results', async () => {
|
||||
await expect(
|
||||
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' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: expect.arrayContaining([
|
||||
searchResults: expect.arrayContaining([
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'a-post',
|
||||
@ -216,7 +231,7 @@ und hinter tausend Stäben keine Welt.`,
|
||||
variables = { query: 'tee-ei' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
searchResults: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'g-post',
|
||||
@ -235,7 +250,7 @@ und hinter tausend Stäben keine Welt.`,
|
||||
variables = { query: '„teeei“' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
searchResults: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'g-post',
|
||||
@ -256,7 +271,7 @@ und hinter tausend Stäben keine Welt.`,
|
||||
variables = { query: '(a - b)²' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
searchResults: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'c-post',
|
||||
@ -277,7 +292,7 @@ und hinter tausend Stäben keine Welt.`,
|
||||
variables = { query: '(a-b)²' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
searchResults: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'c-post',
|
||||
@ -298,7 +313,7 @@ und hinter tausend Stäben keine Welt.`,
|
||||
variables = { query: '+ b² 2.' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
searchResults: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'c-post',
|
||||
@ -321,7 +336,7 @@ und hinter tausend Stäben keine Welt.`,
|
||||
variables = { query: 'der panther' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
searchResults: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'd-post',
|
||||
@ -349,7 +364,7 @@ und hinter tausend Stäben keine Welt.`,
|
||||
variables = { query: 'Vorü Subs' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: expect.arrayContaining([
|
||||
searchResults: expect.arrayContaining([
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'd-post',
|
||||
@ -395,7 +410,7 @@ und hinter tausend Stäben keine Welt.`,
|
||||
variables = { query: '-maria-' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: expect.arrayContaining([
|
||||
searchResults: expect.arrayContaining([
|
||||
{
|
||||
__typename: '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', () => {
|
||||
beforeAll(async () => {
|
||||
const mutedUser = await Factory.build('user', {
|
||||
@ -440,7 +577,7 @@ und hinter tausend Stäben keine Welt.`,
|
||||
variables = { query: 'beitrag' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: expect.not.arrayContaining([
|
||||
searchResults: expect.not.arrayContaining([
|
||||
{
|
||||
__typename: 'Post',
|
||||
id: 'muted-post',
|
||||
@ -465,7 +602,7 @@ und hinter tausend Stäben keine Welt.`,
|
||||
variables = { query: 'myha' }
|
||||
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
findResources: [
|
||||
searchResults: [
|
||||
{
|
||||
__typename: 'Tag',
|
||||
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) {
|
||||
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) {
|
||||
|
||||
@ -265,9 +265,9 @@ describe('UpdateUser', () => {
|
||||
})
|
||||
|
||||
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({
|
||||
data: { UpdateUser: { locationName: 'Hamburg, New Jersey, United States of America' } },
|
||||
data: { UpdateUser: { locationName: 'Hamburg, New Jersey, United States' } },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,5 +1,23 @@
|
||||
union SearchResult = Post | User | Tag
|
||||
|
||||
type Query {
|
||||
findResources(query: String!, limit: Int = 5): [SearchResult]!
|
||||
type postSearchResults {
|
||||
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 => {
|
||||
cy.get(".ds-heading").should("contain", "Users");
|
||||
cy.get(".search-heading").should("contain", "Users");
|
||||
table.hashes().forEach(({ 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(
|
||||
"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:
|
||||
| id | title | content |
|
||||
| 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:
|
||||
| slug | name | id |
|
||||
| search-for-me | Search for me | user-for-search |
|
||||
@ -23,10 +23,10 @@ Feature: Search
|
||||
| title |
|
||||
| 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
|
||||
Then I should have one item in the select dropdown
|
||||
Then I should see the following posts in the select dropdown:
|
||||
Then I should see the search results page
|
||||
Then I should see the following posts on the search results page
|
||||
| title |
|
||||
| 101 Essays that will change the way you think |
|
||||
|
||||
|
||||
@ -16,13 +16,14 @@ h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p {
|
||||
p,
|
||||
li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
ol,
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@ -251,7 +251,7 @@ $size-ribbon: 6px;
|
||||
*/
|
||||
|
||||
$size-width-filter-sidebar: 85px;
|
||||
$size-width-paginate: 100px;
|
||||
$size-width-paginate: 200px;
|
||||
$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
|
||||
|
||||
describe('PaginationButtons.vue', () => {
|
||||
let propsData = {}
|
||||
const propsData = {
|
||||
showPageCounter: true,
|
||||
activePage: 1,
|
||||
activeResourceCount: 57,
|
||||
}
|
||||
let wrapper
|
||||
let nextButton
|
||||
let backButton
|
||||
const mocks = {
|
||||
$t: jest.fn(),
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(PaginationButtons, { propsData, localVue })
|
||||
return mount(PaginationButtons, { mocks, propsData, localVue })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
describe('next button', () => {
|
||||
beforeEach(() => {
|
||||
propsData.hasNext = true
|
||||
wrapper = Wrapper()
|
||||
nextButton = wrapper.find('[data-test="next-button"]')
|
||||
})
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
describe('next button', () => {
|
||||
it('is disabled by default', () => {
|
||||
propsData = {}
|
||||
wrapper = Wrapper()
|
||||
nextButton = wrapper.find('[data-test="next-button"]')
|
||||
const nextButton = wrapper.find('[data-test="next-button"]')
|
||||
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()
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
describe('back button', () => {
|
||||
beforeEach(() => {
|
||||
propsData.hasPrevious = true
|
||||
wrapper = Wrapper()
|
||||
backButton = wrapper.find('[data-test="previous-button"]')
|
||||
})
|
||||
|
||||
describe('previous button', () => {
|
||||
it('is disabled by default', () => {
|
||||
propsData = {}
|
||||
wrapper = Wrapper()
|
||||
backButton = wrapper.find('[data-test="previous-button"]')
|
||||
expect(backButton.attributes().disabled).toEqual('disabled')
|
||||
const previousButton = wrapper.find('[data-test="previous-button"]')
|
||||
expect(previousButton.attributes().disabled).toEqual('disabled')
|
||||
})
|
||||
|
||||
it('is enabled if hasPrevious is true', () => {
|
||||
expect(backButton.attributes().disabled).toBeUndefined()
|
||||
it('is enabled if hasPrevious is true', async () => {
|
||||
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 () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
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>
|
||||
<div class="pagination-buttons">
|
||||
<base-button
|
||||
@click="$emit('back')"
|
||||
class="previous-button"
|
||||
:disabled="!hasPrevious"
|
||||
icon="arrow-left"
|
||||
circle
|
||||
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
|
||||
@click="$emit('next')"
|
||||
class="next-button"
|
||||
:disabled="!hasNext"
|
||||
icon="arrow-right"
|
||||
circle
|
||||
data-test="next-button"
|
||||
@click="$emit('next')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -20,12 +28,31 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 24,
|
||||
},
|
||||
hasNext: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hasPrevious: {
|
||||
type: Boolean,
|
||||
},
|
||||
activePage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalResultCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
activeResourceCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
showPageCounter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
@ -39,4 +66,10 @@ export default {
|
||||
width: $size-width-paginate;
|
||||
margin: $space-x-small auto;
|
||||
}
|
||||
|
||||
.pagination-pageCount {
|
||||
justify-content: space-around;
|
||||
|
||||
margin: 8px auto;
|
||||
}
|
||||
</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>
|
||||
|
||||
<script>
|
||||
import { findResourcesQuery } from '~/graphql/Search.js'
|
||||
import { searchQuery } from '~/graphql/Search.js'
|
||||
import SearchableInput from '~/components/generic/SearchableInput/SearchableInput.vue'
|
||||
|
||||
export default {
|
||||
@ -28,14 +28,14 @@ export default {
|
||||
this.pending = true
|
||||
try {
|
||||
const {
|
||||
data: { findResources },
|
||||
data: { searchResults },
|
||||
} = await this.$apollo.query({
|
||||
query: findResourcesQuery,
|
||||
query: searchQuery,
|
||||
variables: {
|
||||
query: value,
|
||||
},
|
||||
})
|
||||
this.searchResults = findResources
|
||||
this.searchResults = searchResults
|
||||
} catch (error) {
|
||||
this.searchResults = []
|
||||
} finally {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ds-heading soft size="h5" class="search-heading">
|
||||
{{ $t(`search.heading.${resourceType}`) }}
|
||||
{{ $t(`search.heading.${resourceType}`, {}, 2) }}
|
||||
</ds-heading>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
@ -60,13 +60,6 @@ describe('SearchableInput.vue', () => {
|
||||
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', () => {
|
||||
const spy = jest.spyOn(wrapper.vm, 'onDelete')
|
||||
select.trigger('input')
|
||||
@ -117,5 +110,15 @@ describe('SearchableInput.vue', () => {
|
||||
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.delay)
|
||||
},
|
||||
/**
|
||||
* TODO: on enter we should go to a dedicated search page!?
|
||||
*/
|
||||
onEnter(event) {
|
||||
clearTimeout(this.searchProcess)
|
||||
if (!this.loading) {
|
||||
this.previousSearchTerm = this.unprocessedSearchInput
|
||||
this.$emit('query', this.unprocessedSearchInput)
|
||||
}
|
||||
this.$router.push({
|
||||
path: '/search/search-results',
|
||||
query: { search: this.unprocessedSearchInput },
|
||||
})
|
||||
this.$emit('clearSearch')
|
||||
},
|
||||
onDelete(event) {
|
||||
clearTimeout(this.searchProcess)
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
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}
|
||||
${postFragment}
|
||||
|
||||
query($query: String!) {
|
||||
findResources(query: $query, limit: 5) {
|
||||
searchResults(query: $query, limit: 5) {
|
||||
__typename
|
||||
... on 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": {
|
||||
"subheader": "für das Soziale Netzwerk von {ORGANIZATION_NAME}"
|
||||
},
|
||||
@ -546,13 +549,18 @@
|
||||
},
|
||||
"search": {
|
||||
"failed": "Nichts gefunden",
|
||||
"for": "Suche nach ",
|
||||
"heading": {
|
||||
"Post": "Beiträge",
|
||||
"Tag": "Hashtags",
|
||||
"User": "Benutzer"
|
||||
"Post": "Beitrag ::: Beiträge",
|
||||
"Tag": "Hashtag ::: Hashtags",
|
||||
"User": "Benutzer ::: Benutzer"
|
||||
},
|
||||
"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": {
|
||||
"blocked-users": {
|
||||
|
||||
@ -75,6 +75,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"client-only": {
|
||||
"loading": "Loading …"
|
||||
},
|
||||
"code-of-conduct": {
|
||||
"subheader": "for the social network of {ORGANIZATION_NAME}"
|
||||
},
|
||||
@ -546,13 +549,18 @@
|
||||
},
|
||||
"search": {
|
||||
"failed": "Nothing found",
|
||||
"for": "Searching for ",
|
||||
"heading": {
|
||||
"Post": "Posts",
|
||||
"Tag": "Hashtags",
|
||||
"User": "Users"
|
||||
"Post": "Post ::: Posts",
|
||||
"Tag": "Hashtag ::: Hashtags",
|
||||
"User": "User ::: Users"
|
||||
},
|
||||
"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": {
|
||||
"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="post"
|
||||
@removePostFromList="deletePost"
|
||||
@pinPost="pinPost"
|
||||
@unpinPost="unpinPost"
|
||||
@removePostFromList="posts = removePostFromList(post, posts)"
|
||||
@pinPost="pinPost(post, refetchPostList)"
|
||||
@unpinPost="unpinPost(post, refetchPostList)"
|
||||
/>
|
||||
</masonry-grid-item>
|
||||
</template>
|
||||
@ -64,6 +64,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import postListActions from '~/mixins/postListActions'
|
||||
// import DonationInfo from '~/components/DonationInfo/DonationInfo.vue'
|
||||
import HashtagsFilter from '~/components/HashtagsFilter/HashtagsFilter.vue'
|
||||
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 { mapGetters, mapMutations } from 'vuex'
|
||||
import { filterPosts } from '~/graphql/PostQuery.js'
|
||||
import PostMutations from '~/graphql/PostMutations'
|
||||
import UpdateQuery from '~/components/utils/UpdateQuery'
|
||||
import links from '~/constants/links.js'
|
||||
|
||||
@ -85,6 +85,7 @@ export default {
|
||||
MasonryGrid,
|
||||
MasonryGridItem,
|
||||
},
|
||||
mixins: [postListActions],
|
||||
data() {
|
||||
const { hashtag = null } = this.$route.query
|
||||
return {
|
||||
@ -162,41 +163,14 @@ export default {
|
||||
updateQuery: UpdateQuery(this, { $state, pageKey: 'Post' }),
|
||||
})
|
||||
},
|
||||
deletePost(deletedPost) {
|
||||
this.posts = this.posts.filter((post) => {
|
||||
return post.id !== deletedPost.id
|
||||
})
|
||||
},
|
||||
resetPostList() {
|
||||
this.offset = 0
|
||||
this.posts = []
|
||||
this.hasMore = true
|
||||
},
|
||||
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.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))
|
||||
refetchPostList() {
|
||||
this.resetPostList()
|
||||
this.$apollo.queries.Post.refetch()
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
|
||||
@ -90,7 +90,7 @@ describe('PostIndex', () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
@ -27,7 +27,9 @@
|
||||
<post-teaser
|
||||
:post="relatedPost"
|
||||
: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>
|
||||
@ -37,6 +39,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import postListActions from '~/mixins/postListActions'
|
||||
import HcEmpty from '~/components/Empty/Empty'
|
||||
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
|
||||
import HcCategory from '~/components/Category'
|
||||
@ -47,10 +50,6 @@ import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
||||
import { sortTagsAlphabetically } from '~/components/utils/PostHelpers'
|
||||
|
||||
export default {
|
||||
transition: {
|
||||
name: 'slide-up',
|
||||
mode: 'out-in',
|
||||
},
|
||||
components: {
|
||||
PostTeaser,
|
||||
HcCategory,
|
||||
@ -59,6 +58,11 @@ export default {
|
||||
MasonryGrid,
|
||||
MasonryGridItem,
|
||||
},
|
||||
transition: {
|
||||
name: 'slide-up',
|
||||
mode: 'out-in',
|
||||
},
|
||||
mixins: [postListActions],
|
||||
computed: {
|
||||
post() {
|
||||
return this.Post ? this.Post[0] || {} : {}
|
||||
@ -68,10 +72,8 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removePostFromList(deletedPost) {
|
||||
this.post.relatedContributions = this.post.relatedContributions.filter((contribution) => {
|
||||
return contribution.id !== deletedPost.id
|
||||
})
|
||||
refetchPostList() {
|
||||
this.$apollo.queries.Post.refetch()
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
|
||||
@ -108,7 +108,9 @@
|
||||
|
||||
<ds-flex-item :width="{ base: '100%', sm: 3, md: 5, lg: 3 }">
|
||||
<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">
|
||||
<ul class="Tabs">
|
||||
<li class="Tabs__tab pointer" :class="{ active: tabActive === 'post' }">
|
||||
@ -150,8 +152,9 @@
|
||||
</li>
|
||||
</ul>
|
||||
</base-card>
|
||||
</ds-grid-item>
|
||||
</ds-grid-item> -->
|
||||
|
||||
<!-- feed -->
|
||||
<ds-grid-item :row-span="2" column-span="fullWidth">
|
||||
<ds-space centered>
|
||||
<nuxt-link :to="{ name: 'post-create' }">
|
||||
@ -181,9 +184,9 @@
|
||||
<post-teaser
|
||||
:post="post"
|
||||
:width="{ base: '100%', md: '100%', xl: '50%' }"
|
||||
@removePostFromList="removePostFromList"
|
||||
@pinPost="pinPost"
|
||||
@unpinPost="unpinPost"
|
||||
@removePostFromList="posts = removePostFromList(post, posts)"
|
||||
@pinPost="pinPost(post, refetchPostList)"
|
||||
@unpinPost="unpinPost(post, refetchPostList)"
|
||||
/>
|
||||
</masonry-grid-item>
|
||||
</template>
|
||||
@ -210,6 +213,7 @@
|
||||
|
||||
<script>
|
||||
import uniqBy from 'lodash/uniqBy'
|
||||
import postListActions from '~/mixins/postListActions'
|
||||
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
|
||||
import HcFollowButton from '~/components/FollowButton.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 MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
|
||||
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
||||
import NewTabNavigation from '~/components/_new/generic/TabNavigation/NewTabNavigation'
|
||||
import { profilePagePosts } from '~/graphql/PostQuery'
|
||||
import UserQuery from '~/graphql/User'
|
||||
import { muteUser, unmuteUser } from '~/graphql/settings/MutedUsers'
|
||||
@ -251,7 +256,9 @@ export default {
|
||||
MasonryGrid,
|
||||
MasonryGridItem,
|
||||
FollowList,
|
||||
NewTabNavigation,
|
||||
},
|
||||
mixins: [postListActions],
|
||||
transition: {
|
||||
name: 'slide-up',
|
||||
mode: 'out-in',
|
||||
@ -286,17 +293,36 @@ export default {
|
||||
const { slug } = this.user || {}
|
||||
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: {
|
||||
removePostFromList(deletedPost) {
|
||||
this.posts = this.posts.filter((post) => {
|
||||
return post.id !== deletedPost.id
|
||||
})
|
||||
},
|
||||
handleTab(tab) {
|
||||
this.tabActive = tab
|
||||
this.filter = tabToFilterMapping({ tab, id: this.$route.params.id })
|
||||
this.resetPostList()
|
||||
if (this.tabActive !== tab) {
|
||||
this.tabActive = tab
|
||||
this.filter = tabToFilterMapping({ tab, id: this.$route.params.id })
|
||||
this.resetPostList()
|
||||
}
|
||||
},
|
||||
uniq(items, field = 'id') {
|
||||
return uniqBy(items, field)
|
||||
@ -321,6 +347,10 @@ export default {
|
||||
this.posts = []
|
||||
this.hasMore = true
|
||||
},
|
||||
refetchPostList() {
|
||||
this.resetPostList()
|
||||
this.$apollo.queries.profilePagePosts.refetch()
|
||||
},
|
||||
async muteUser(user) {
|
||||
try {
|
||||
await this.$apollo.mutate({ mutation: muteUser(), variables: { id: user.id } })
|
||||
@ -361,40 +391,6 @@ export default {
|
||||
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 }) {
|
||||
/*
|
||||
* Note: followedByCountStartValue is updated to avoid counting from 0 when follow/unfollow
|
||||
@ -457,33 +453,33 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
// Wolle .pointer {
|
||||
// cursor: pointer;
|
||||
// }
|
||||
|
||||
.Tabs {
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
// Wolle .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;
|
||||
// &__tab {
|
||||
// text-align: center;
|
||||
// height: 100%;
|
||||
// flex-grow: 1;
|
||||
|
||||
&:hover {
|
||||
border-bottom: 2px solid #c9c6ce;
|
||||
}
|
||||
// &:hover {
|
||||
// border-bottom: 2px solid #c9c6ce;
|
||||
// }
|
||||
|
||||
&.active {
|
||||
border-bottom: 2px solid #17b53f;
|
||||
}
|
||||
}
|
||||
}
|
||||
// &.active {
|
||||
// border-bottom: 2px solid #17b53f;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
.profile-avatar.user-avatar {
|
||||
margin: auto;
|
||||
margin-top: -60px;
|
||||
@ -495,26 +491,26 @@ export default {
|
||||
right: $space-x-small;
|
||||
}
|
||||
}
|
||||
.profile-top-navigation {
|
||||
position: sticky;
|
||||
top: 53px;
|
||||
z-index: 2;
|
||||
}
|
||||
.ds-tab-nav.base-card {
|
||||
padding: 0;
|
||||
// Wolle .profile-top-navigation {
|
||||
// position: sticky;
|
||||
// top: 53px;
|
||||
// z-index: 2;
|
||||
// }
|
||||
// Wolle .ds-tab-nav.base-card {
|
||||
// padding: 0;
|
||||
|
||||
.ds-tab-nav-item {
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// .ds-tab-nav-item {
|
||||
// &.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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
.profile-post-add-button {
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user