diff --git a/backend/src/db/migrate/store.js b/backend/src/db/migrate/store.js index 8bc73b511..02e048c07 100644 --- a/backend/src/db/migrate/store.js +++ b/backend/src/db/migrate/store.js @@ -12,6 +12,7 @@ class Store { [ 'CALL db.index.fulltext.createNodeIndex("post_fulltext_search",["Post"],["title", "content"])', 'CALL db.index.fulltext.createNodeIndex("user_fulltext_search",["User"],["name", "slug"])', + 'CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"])', ].map(statement => txc.run(statement)), ) }) diff --git a/backend/src/db/migrate/template.js b/backend/src/db/migrate/template.js index 9adb0786d..72bfc9b1b 100644 --- a/backend/src/db/migrate/template.js +++ b/backend/src/db/migrate/template.js @@ -40,6 +40,7 @@ export async function down(next) { await transaction.rollback() // eslint-disable-next-line no-console console.log('rolled back') + throw new Error(error) } finally { session.close() } diff --git a/backend/src/db/migrations/20200207080200-fulltext_index_for_tags.js b/backend/src/db/migrations/20200207080200-fulltext_index_for_tags.js new file mode 100644 index 000000000..5064a8b17 --- /dev/null +++ b/backend/src/db/migrations/20200207080200-fulltext_index_for_tags.js @@ -0,0 +1,51 @@ +import { getDriver } from '../../db/neo4j' + +export const description = + 'This migration adds a fulltext index for the tags in order to search for Hasthags.' + +export async function up(next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + await transaction.run(` + CALL db.index.fulltext.createNodeIndex("tag_fulltext_search",["Tag"],["id"]) + `) + await transaction.commit() + next() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + session.close() + } +} + +export async function down(next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + + try { + // Implement your migration here. + await transaction.run(` + CALL db.index.fulltext.drop("tag_fulltext_search") + `) + await transaction.commit() + next() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + session.close() + } +} diff --git a/backend/src/schema/resolvers/searches.js b/backend/src/schema/resolvers/searches.js index 5c1e43952..3471c783b 100644 --- a/backend/src/schema/resolvers/searches.js +++ b/backend/src/schema/resolvers/searches.js @@ -41,6 +41,16 @@ export default { 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) const session = context.driver.session() @@ -55,14 +65,25 @@ export default { limit, thisUserId, }) - return Promise.all([postTransactionResponse, userTransactionResponse]) + const tagTransactionResponse = transaction.run(tagCypher, { + query: myQuery, + limit, + }) + return Promise.all([ + postTransactionResponse, + userTransactionResponse, + tagTransactionResponse, + ]) }) try { - const [postResults, userResults] = await searchResultPromise + const [postResults, userResults, tagResults] = await searchResultPromise log(postResults) log(userResults) - return [...postResults.records, ...userResults.records].map(r => r.get('resource')) + log(tagResults) + return [...postResults.records, ...userResults.records, ...tagResults.records].map(r => + r.get('resource'), + ) } finally { session.close() } diff --git a/backend/src/schema/resolvers/searches.spec.js b/backend/src/schema/resolvers/searches.spec.js index c454833b8..081d71e55 100644 --- a/backend/src/schema/resolvers/searches.spec.js +++ b/backend/src/schema/resolvers/searches.spec.js @@ -41,6 +41,9 @@ const searchQuery = gql` slug name } + ... on Tag { + id + } } } ` @@ -439,6 +442,28 @@ und hinter tausend Stäben keine Welt.`, }) }) }) + + describe('adding a tag', () => { + beforeAll(async () => { + await Factory.build('tag', { id: 'myHashtag' }) + }) + + describe('query the first four characters of the tag', () => { + it('finds the tag', async () => { + variables = { query: 'myha' } + await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({ + data: { + findResources: [ + { + __typename: 'Tag', + id: 'myHashtag', + }, + ], + }, + }) + }) + }) + }) }) }) }) diff --git a/backend/src/schema/types/type/Search.gql b/backend/src/schema/types/type/Search.gql index 2c22fa61f..1ce38001d 100644 --- a/backend/src/schema/types/type/Search.gql +++ b/backend/src/schema/types/type/Search.gql @@ -1,4 +1,4 @@ -union SearchResult = Post | User +union SearchResult = Post | User | Tag type Query { findResources(query: String!, limit: Int = 5): [SearchResult]! diff --git a/webapp/components/Hashtag/Hashtag.vue b/webapp/components/Hashtag/Hashtag.vue index 35762c81c..44e5319f0 100644 --- a/webapp/components/Hashtag/Hashtag.vue +++ b/webapp/components/Hashtag/Hashtag.vue @@ -1,5 +1,5 @@ diff --git a/webapp/components/generic/SearchableInput/SearchableInput.spec.js b/webapp/components/generic/SearchableInput/SearchableInput.spec.js index c685ae3e6..4a095304b 100644 --- a/webapp/components/generic/SearchableInput/SearchableInput.spec.js +++ b/webapp/components/generic/SearchableInput/SearchableInput.spec.js @@ -106,6 +106,16 @@ describe('SearchableInput.vue', () => { params: { id: 'u2', slug: 'bob-der-baumeister' }, }) }) + + it('pushes hashtag query params', async () => { + select.element.value = 'Hash' + select.trigger('input') + const tags = wrapper.findAll('.hc-hashtag') + const tag = tags.filter(item => item.text().match(/#Hashtag/)) + tag.trigger('click') + await Vue.nextTick() + expect(mocks.$router.push).toHaveBeenCalledWith('?hashtag=Hashtag') + }) }) }) }) diff --git a/webapp/components/generic/SearchableInput/SearchableInput.story.js b/webapp/components/generic/SearchableInput/SearchableInput.story.js index d969fa976..4b5c42e3b 100644 --- a/webapp/components/generic/SearchableInput/SearchableInput.story.js +++ b/webapp/components/generic/SearchableInput/SearchableInput.story.js @@ -106,6 +106,10 @@ export const searchResults = [ name: 'Tonya Mohr', slug: 'tonya-mohr', }, + { + id: 'Hashtag', + __typename: 'Tag', + }, ] storiesOf('Search Field', module) diff --git a/webapp/components/generic/SearchableInput/SearchableInput.vue b/webapp/components/generic/SearchableInput/SearchableInput.vue index 3260ff082..b0050b429 100644 --- a/webapp/components/generic/SearchableInput/SearchableInput.vue +++ b/webapp/components/generic/SearchableInput/SearchableInput.vue @@ -35,6 +35,12 @@ >

+

+ +

@@ -45,6 +51,7 @@ import { isEmpty } from 'lodash' import SearchHeading from '~/components/generic/SearchHeading/SearchHeading.vue' import SearchPost from '~/components/generic/SearchPost/SearchPost.vue' +import HcHashtag from '~/components/Hashtag/Hashtag.vue' import UserTeaser from '~/components/UserTeaser/UserTeaser.vue' export default { @@ -52,6 +59,7 @@ export default { components: { SearchHeading, SearchPost, + HcHashtag, UserTeaser, }, props: { @@ -138,12 +146,19 @@ export default { isPost(item) { return item.__typename === 'Post' }, + isTag(item) { + return item.__typename === 'Tag' + }, goToResource(item) { this.$nextTick(() => { - this.$router.push({ - name: this.isPost(item) ? 'post-id-slug' : 'profile-id-slug', - params: { id: item.id, slug: item.slug }, - }) + if (!this.isTag(item)) { + this.$router.push({ + name: this.isPost(item) ? 'post-id-slug' : 'profile-id-slug', + params: { id: item.id, slug: item.slug }, + }) + } else { + this.$router.push('?hashtag=' + item.id) + } }) }, }, diff --git a/webapp/graphql/Search.js b/webapp/graphql/Search.js index 9b142b429..ab34b58b6 100644 --- a/webapp/graphql/Search.js +++ b/webapp/graphql/Search.js @@ -19,6 +19,9 @@ export const findResourcesQuery = gql` ... on User { ...user } + ... on Tag { + id + } } } ` diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 524ee58fa..a2ab2246f 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -599,6 +599,7 @@ "failed": "Nichts gefunden", "heading": { "Post": "Beiträge", + "Tag": "Hashtags", "User": "Benutzer" }, "hint": "Wonach suchst Du?", diff --git a/webapp/locales/en.json b/webapp/locales/en.json index e6b8def5a..eb19dfbe7 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -599,6 +599,7 @@ "failed": "Nothing found", "heading": { "Post": "Posts", + "Tag": "Hashtags", "User": "Users" }, "hint": "What are you searching for?", diff --git a/webapp/pages/index.vue b/webapp/pages/index.vue index 8aef52c1d..e9e6bed54 100644 --- a/webapp/pages/index.vue +++ b/webapp/pages/index.vue @@ -130,6 +130,7 @@ export default { return this.$apollo.loading || (this.posts && this.posts.length > 0) }, }, + watchQuery: ['hashtag'], methods: { ...mapMutations({ selectOrder: 'posts/SELECT_ORDER',