mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Fix search by adding result id
- Apollo cache requires an id to commit to their store - add id to each query result - refactor out the store - still can use a lot of refactoring
This commit is contained in:
parent
80e076fc86
commit
ebc5cf392d
@ -1,15 +1,15 @@
|
|||||||
export default {
|
import uuid from 'uuid/v4'
|
||||||
SearchResult: {
|
|
||||||
__resolveType(obj, context, info) {
|
const transformReturnType = record => {
|
||||||
if (obj.encryptedPassword) {
|
return {
|
||||||
return 'User'
|
id: uuid(),
|
||||||
}
|
searchResults: {
|
||||||
if (obj.content) {
|
__typename: record.get('type'),
|
||||||
return 'Post'
|
...record.get('resource').properties,
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
findResources: async (_parent, args, context, _resolveInfo) => {
|
findResources: async (_parent, args, context, _resolveInfo) => {
|
||||||
const query = args.query
|
const query = args.query
|
||||||
@ -21,14 +21,14 @@ export default {
|
|||||||
const userQuery = query.replace(/\s/g, '~ ') + '~'
|
const userQuery = query.replace(/\s/g, '~ ') + '~'
|
||||||
const postCypher = `
|
const postCypher = `
|
||||||
CALL db.index.fulltext.queryNodes('post_fulltext_search', $query)
|
CALL db.index.fulltext.queryNodes('post_fulltext_search', $query)
|
||||||
YIELD node as post, score
|
YIELD node as resource, score
|
||||||
MATCH (post)<-[:WROTE]-(user:User)
|
MATCH (resource)<-[:WROTE]-(user:User)
|
||||||
WHERE score >= 0.2
|
WHERE score >= 0.2
|
||||||
AND NOT user.deleted = true AND NOT user.disabled = true
|
AND NOT user.deleted = true AND NOT user.disabled = true
|
||||||
AND NOT post.deleted = true AND NOT post.disabled = true
|
AND NOT resource.deleted = true AND NOT resource.disabled = true
|
||||||
AND NOT user.id in COALESCE($filter.author_not.id_in, [])
|
AND NOT user.id in COALESCE($filter.author_not.id_in, [])
|
||||||
AND NOT (:User { id: $thisUserId })-[:BLOCKED]->(user)
|
AND NOT (:User { id: $thisUserId })-[:BLOCKED]->(user)
|
||||||
RETURN post
|
RETURN resource, labels(resource)[0] AS type
|
||||||
LIMIT $limit
|
LIMIT $limit
|
||||||
`
|
`
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
@ -41,12 +41,12 @@ export default {
|
|||||||
session.close()
|
session.close()
|
||||||
const userCypher = `
|
const userCypher = `
|
||||||
CALL db.index.fulltext.queryNodes('user_fulltext_search', $query)
|
CALL db.index.fulltext.queryNodes('user_fulltext_search', $query)
|
||||||
YIELD node as user, score
|
YIELD node as resource, score
|
||||||
MATCH (user)
|
MATCH (resource)
|
||||||
WHERE score >= 0.2
|
WHERE score >= 0.2
|
||||||
AND NOT user.deleted = true AND NOT user.disabled = true
|
AND NOT resource.deleted = true AND NOT resource.disabled = true
|
||||||
AND NOT (:User { id: $thisUserId })-[:BLOCKED]->(user)
|
AND NOT (:User { id: $thisUserId })-[:BLOCKED]->(resource)
|
||||||
RETURN user
|
RETURN resource, labels(resource)[0] AS type
|
||||||
LIMIT $limit
|
LIMIT $limit
|
||||||
`
|
`
|
||||||
const userResults = await session.run(userCypher, {
|
const userResults = await session.run(userCypher, {
|
||||||
@ -57,10 +57,8 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
session.close()
|
session.close()
|
||||||
const result = []
|
const result = postResults.records.concat(userResults.records).map(transformReturnType)
|
||||||
postResults.records
|
|
||||||
.concat(userResults.records)
|
|
||||||
.forEach(record => result.push(record._fields[0].properties))
|
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
union SearchResult = Post | User
|
type SearchResult {
|
||||||
|
id: ID!
|
||||||
|
searchResults: ResourceResults
|
||||||
|
}
|
||||||
|
|
||||||
|
union ResourceResults = Post | User
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
findResources(query: String!, limit: Int = 5): [SearchResult]!
|
findResources(query: String!, limit: Int = 5): [SearchResult]!
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
:no-options-available="emptyText"
|
:no-options-available="emptyText"
|
||||||
:icon-right="isActive ? 'close' : null"
|
:icon-right="isActive ? 'close' : null"
|
||||||
:filter="item => item"
|
:filter="item => item"
|
||||||
:options="results"
|
:options="searchResults"
|
||||||
:auto-reset-search="!searchValue"
|
:auto-reset-search="!searchValue"
|
||||||
:placeholder="$t('search.placeholder')"
|
:placeholder="$t('search.placeholder')"
|
||||||
:loading="pending"
|
:loading="pending"
|
||||||
@ -45,7 +45,7 @@
|
|||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
</ds-flex>
|
</ds-flex>
|
||||||
<ds-flex v-if="option.__typename === 'User'">
|
<ds-flex v-if="option.__typename === 'User'">
|
||||||
<ds-flex-item class="search-option" :class="{'extra-space': isFirstOfType(option)}">
|
<ds-flex-item class="search-option" :class="{ 'extra-space': isFirstOfType(option) }">
|
||||||
<ds-avatar class="avatar" name="option.name" image="option.avatar" />
|
<ds-avatar class="avatar" name="option.name" image="option.avatar" />
|
||||||
<div>
|
<div>
|
||||||
<ds-text class="userinfo">
|
<ds-text class="userinfo">
|
||||||
@ -93,6 +93,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
|
import { findResourcesQuery } from '~/graphql/Search.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SearchInput',
|
name: 'SearchInput',
|
||||||
props: {
|
props: {
|
||||||
@ -100,30 +102,22 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'nav-search',
|
default: 'nav-search',
|
||||||
},
|
},
|
||||||
value: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
results: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
delay: {
|
delay: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 300,
|
default: 300,
|
||||||
},
|
},
|
||||||
pending: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searchProcess: null,
|
searchProcess: null,
|
||||||
|
value: '',
|
||||||
|
pending: false,
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
lastSearchTerm: '',
|
lastSearchTerm: '',
|
||||||
unprocessedSearchInput: '',
|
unprocessedSearchInput: '',
|
||||||
searchValue: '',
|
searchValue: '',
|
||||||
|
quickValue: '',
|
||||||
|
searchResults: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -136,21 +130,42 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async query(value) {
|
async query(value) {
|
||||||
if (isEmpty(value) || value.length < 3) {
|
if (
|
||||||
this.clear()
|
isEmpty(value) ||
|
||||||
return
|
value.length < 3 ||
|
||||||
}
|
this.quickValue.toLowerCase() === value.toLowerCase()
|
||||||
this.$emit('search', value)
|
) {
|
||||||
|
this.clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.quickValue = value
|
||||||
|
this.pending = true
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { findResources },
|
||||||
|
} = await this.$apollo.query({
|
||||||
|
query: findResourcesQuery,
|
||||||
|
variables: {
|
||||||
|
query: value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const searchResults = findResources.map(searchResult => searchResult.searchResults)
|
||||||
|
this.searchResults = searchResults
|
||||||
|
} catch (error) {
|
||||||
|
this.searchResults = []
|
||||||
|
} finally {
|
||||||
|
this.pending = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
handleInput(e) {
|
handleInput(e) {
|
||||||
clearTimeout(this.searchProcess)
|
clearTimeout(this.searchProcess)
|
||||||
const value = e.target ? e.target.value.trim() : ''
|
this.value = e.target ? e.target.value.trim() : ''
|
||||||
this.isOpen = true
|
this.isOpen = true
|
||||||
this.unprocessedSearchInput = value
|
this.unprocessedSearchInput = this.value
|
||||||
this.searchProcess = setTimeout(() => {
|
this.searchProcess = setTimeout(() => {
|
||||||
this.lastSearchTerm = value
|
this.lastSearchTerm = this.value
|
||||||
this.query(value)
|
this.query(this.value)
|
||||||
}, this.delay)
|
}, this.delay)
|
||||||
},
|
},
|
||||||
onSelect(item) {
|
onSelect(item) {
|
||||||
@ -193,8 +208,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
clear() {
|
clear() {
|
||||||
this.$emit('clear')
|
this.pending = false
|
||||||
clearTimeout(this.searchProcess)
|
this.searchResults = []
|
||||||
|
this.quickValue = ''
|
||||||
|
this.clearTimeout(this.searchProcess)
|
||||||
this.isOpen = false
|
this.isOpen = false
|
||||||
this.unprocessedSearchInput = ''
|
this.unprocessedSearchInput = ''
|
||||||
this.lastSearchTerm = ''
|
this.lastSearchTerm = ''
|
||||||
@ -202,8 +219,8 @@ export default {
|
|||||||
},
|
},
|
||||||
isFirstOfType(option) {
|
isFirstOfType(option) {
|
||||||
return (
|
return (
|
||||||
this.results.findIndex(o => o === option) ===
|
this.searchResults.findIndex(o => o === option) ===
|
||||||
this.results.findIndex(o => o.__typename === option.__typename)
|
this.searchResults.findIndex(o => o.__typename === option.__typename)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -211,109 +228,109 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.search {
|
.search {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
$padding-left: $space-x-small;
|
$padding-left: $space-x-small;
|
||||||
.search-option-label {
|
.search-option-label {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
padding-left: $padding-left;
|
padding-left: $padding-left;
|
||||||
}
|
}
|
||||||
.search-option-meta {
|
.search-option-meta {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
.ds-flex {
|
.ds-flex {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&,
|
&,
|
||||||
.ds-select-dropdown {
|
.ds-select-dropdown {
|
||||||
transition: box-shadow 100ms;
|
transition: box-shadow 100ms;
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
}
|
}
|
||||||
&.is-open {
|
&.is-open {
|
||||||
.ds-select-dropdown {
|
.ds-select-dropdown {
|
||||||
box-shadow: $box-shadow-x-large;
|
box-shadow: $box-shadow-x-large;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ds-select-dropdown-message {
|
.ds-select-dropdown-message {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
padding-left: $padding-left;
|
padding-left: $padding-left;
|
||||||
}
|
}
|
||||||
.search-clear-btn {
|
.search-clear-btn {
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 36px;
|
width: 36px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.search-meta {
|
.search-meta {
|
||||||
float: right;
|
float: right;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
word-wrap: none;
|
word-wrap: none;
|
||||||
.base-icon {
|
.base-icon {
|
||||||
vertical-align: sub;
|
vertical-align: sub;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ds-select {
|
.ds-select {
|
||||||
z-index: $z-index-dropdown + 1;
|
z-index: $z-index-dropdown + 1;
|
||||||
}
|
}
|
||||||
.ds-select-option-hover {
|
.ds-select-option-hover {
|
||||||
.ds-text-size-small,
|
.ds-text-size-small,
|
||||||
.ds-text-size-small-x {
|
.ds-text-size-small-x {
|
||||||
color: $text-color-soft;
|
color: $text-color-soft;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.field {
|
.field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.control {
|
.control {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.search-option-heading {
|
.search-option-heading {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
margin: -8px;
|
margin: -8px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
.avatar {
|
.avatar {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
float: left;
|
float: left;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.userinfo {
|
.userinfo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
> .ds-text {
|
> .ds-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: $space-xx-small;
|
margin-left: $space-xx-small;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.user {
|
.user {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
&:hover,
|
&:hover,
|
||||||
&.active {
|
&.active {
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.username {
|
.username {
|
||||||
color: #17b53f;
|
color: #17b53f;
|
||||||
}
|
}
|
||||||
.extra-space {
|
.extra-space {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
29
webapp/graphql/Search.js
Normal file
29
webapp/graphql/Search.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const findResourcesQuery = gql`
|
||||||
|
query($query: String!) {
|
||||||
|
findResources(query: $query, limit: 5) {
|
||||||
|
id
|
||||||
|
searchResults {
|
||||||
|
__typename
|
||||||
|
... on Post {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
slug
|
||||||
|
commentsCount
|
||||||
|
shoutedCount
|
||||||
|
createdAt
|
||||||
|
author {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on User {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -21,15 +21,7 @@
|
|||||||
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||||
>
|
>
|
||||||
<div id="nav-search-box" v-if="isLoggedIn">
|
<div id="nav-search-box" v-if="isLoggedIn">
|
||||||
<search-input
|
<search-input id="nav-search" :delay="300" @select="goToResource" />
|
||||||
id="nav-search"
|
|
||||||
:delay="300"
|
|
||||||
:pending="quickSearchPending"
|
|
||||||
:results="quickSearchResults"
|
|
||||||
@clear="quickSearchClear"
|
|
||||||
@search="value => quickSearch({ value })"
|
|
||||||
@select="goToResource"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
<ds-flex-item
|
<ds-flex-item
|
||||||
@ -122,8 +114,6 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
isLoggedIn: 'auth/isLoggedIn',
|
isLoggedIn: 'auth/isLoggedIn',
|
||||||
quickSearchResults: 'search/quickResults',
|
|
||||||
quickSearchPending: 'search/quickPending',
|
|
||||||
}),
|
}),
|
||||||
showFilterPostsDropdown() {
|
showFilterPostsDropdown() {
|
||||||
const [firstRoute] = this.$route.matched
|
const [firstRoute] = this.$route.matched
|
||||||
|
|||||||
@ -1,92 +0,0 @@
|
|||||||
import gql from 'graphql-tag'
|
|
||||||
import isString from 'lodash/isString'
|
|
||||||
|
|
||||||
export const state = () => {
|
|
||||||
return {
|
|
||||||
quickResults: [],
|
|
||||||
quickPending: false,
|
|
||||||
quickValue: '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mutations = {
|
|
||||||
SET_QUICK_RESULTS(state, results) {
|
|
||||||
state.quickResults = results || []
|
|
||||||
state.quickPending = false
|
|
||||||
},
|
|
||||||
SET_QUICK_PENDING(state, pending) {
|
|
||||||
state.quickPending = pending
|
|
||||||
},
|
|
||||||
SET_QUICK_VALUE(state, value) {
|
|
||||||
state.quickValue = value
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getters = {
|
|
||||||
quickResults(state) {
|
|
||||||
return state.quickResults
|
|
||||||
},
|
|
||||||
quickPending(state) {
|
|
||||||
return state.quickPending
|
|
||||||
},
|
|
||||||
quickValue(state) {
|
|
||||||
return state.quickValue
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
async quickSearch({ commit, getters }, { value }) {
|
|
||||||
value = isString(value) ? value.trim() : ''
|
|
||||||
const lastVal = getters.quickValue
|
|
||||||
if (value.length < 3 || lastVal.toLowerCase() === value.toLowerCase()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
commit('SET_QUICK_VALUE', value)
|
|
||||||
commit('SET_QUICK_PENDING', true)
|
|
||||||
await this.app.apolloProvider.defaultClient
|
|
||||||
.query({
|
|
||||||
query: gql`
|
|
||||||
query findResources($query: String!) {
|
|
||||||
findResources(query: $query, limit: 5) {
|
|
||||||
__typename
|
|
||||||
... on Post {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
slug
|
|
||||||
commentsCount
|
|
||||||
shoutedCount
|
|
||||||
createdAt
|
|
||||||
author {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
... on User {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
slug
|
|
||||||
avatar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
variables: {
|
|
||||||
query: value,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(res => {
|
|
||||||
commit('SET_QUICK_RESULTS', res.data.findResources || [])
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
commit('SET_QUICK_RESULTS', [])
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
commit('SET_QUICK_PENDING', false)
|
|
||||||
})
|
|
||||||
return getters.quickResults
|
|
||||||
},
|
|
||||||
async quickClear({ commit }) {
|
|
||||||
commit('SET_QUICK_PENDING', false)
|
|
||||||
commit('SET_QUICK_RESULTS', [])
|
|
||||||
commit('SET_QUICK_VALUE', '')
|
|
||||||
},
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user