mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #264 from Human-Connection/37-full-text-search-top-bar
37 full text search top bar
This commit is contained in:
commit
0c2a228b47
3
.gitignore
vendored
3
.gitignore
vendored
@ -81,3 +81,6 @@ static/uploads
|
||||
cypress/videos
|
||||
cypress/screenshots/
|
||||
cypress.env.json
|
||||
|
||||
# Apple macOS folder attribute file
|
||||
.DS_Store
|
||||
|
||||
@ -12,11 +12,11 @@
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
<ds-icon
|
||||
style="margin-top: -2px; margin-right: 2px;"
|
||||
style="margin-right: 2px;"
|
||||
name="globe"
|
||||
/> {{ current.code.toUpperCase() }}
|
||||
<ds-icon
|
||||
style="margin-top: -2px; margin-left: 2px"
|
||||
style="margin-left: 2px"
|
||||
size="xx-small"
|
||||
name="angle-down"
|
||||
/>
|
||||
|
||||
144
components/SearchInput.spec.js
Normal file
144
components/SearchInput.spec.js
Normal file
@ -0,0 +1,144 @@
|
||||
import { mount, createLocalVue } from '@vue/test-utils'
|
||||
import SearchInput from './SearchInput.vue'
|
||||
import Vuex from 'vuex'
|
||||
import Styleguide from '@human-connection/styleguide'
|
||||
const localVue = createLocalVue()
|
||||
|
||||
localVue.use(Vuex)
|
||||
localVue.use(Styleguide)
|
||||
localVue.filter('truncate', () => 'truncated string')
|
||||
localVue.filter('dateTime', () => Date.now)
|
||||
|
||||
describe('SearchInput.vue', () => {
|
||||
let wrapper
|
||||
let mocks
|
||||
let propsData
|
||||
|
||||
beforeEach(() => {
|
||||
propsData = {}
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
const Wrapper = () => {
|
||||
mocks = {
|
||||
$t: () => {}
|
||||
}
|
||||
return mount(SearchInput, { mocks, localVue, propsData })
|
||||
}
|
||||
|
||||
it('renders', () => {
|
||||
expect(Wrapper().is('div')).toBe(true)
|
||||
})
|
||||
|
||||
it('has id "nav-search"', () => {
|
||||
expect(Wrapper().contains('#nav-search')).toBe(true)
|
||||
})
|
||||
|
||||
it('defaults to an empty value', () => {
|
||||
expect(Wrapper().vm.value).toBe('')
|
||||
})
|
||||
|
||||
it('defaults to id "nav-search"', () => {
|
||||
expect(Wrapper().vm.id).toBe('nav-search')
|
||||
})
|
||||
|
||||
it('default to a 300 millisecond delay from the time the user stops typing to when the search starts', () => {
|
||||
expect(Wrapper().vm.delay).toEqual(300)
|
||||
})
|
||||
|
||||
it('defaults to an empty array as results', () => {
|
||||
expect(Wrapper().vm.results).toEqual([])
|
||||
})
|
||||
|
||||
it('defaults to pending false, as in the search is not pending', () => {
|
||||
expect(Wrapper().vm.pending).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts values as a string', () => {
|
||||
propsData = { value: 'abc' }
|
||||
const wrapper = Wrapper()
|
||||
expect(wrapper.vm.value).toEqual('abc')
|
||||
})
|
||||
|
||||
describe('testing custom functions', () => {
|
||||
let select
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
select = wrapper.find('.ds-select')
|
||||
select.trigger('focus')
|
||||
select.element.value = 'abcd'
|
||||
})
|
||||
|
||||
it('opens the select dropdown when focused on', () => {
|
||||
expect(wrapper.vm.isOpen).toBe(true)
|
||||
})
|
||||
|
||||
it('opens the select dropdown and blurs after focused on', () => {
|
||||
select.trigger('blur')
|
||||
expect(wrapper.vm.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('is clearable', () => {
|
||||
select.trigger('input')
|
||||
select.trigger('keyup.esc')
|
||||
expect(wrapper.emitted().clear.length).toBe(1)
|
||||
})
|
||||
|
||||
it('changes the unprocessedSearchInput as the value changes', () => {
|
||||
select.trigger('input')
|
||||
expect(wrapper.vm.unprocessedSearchInput).toBe('abcd')
|
||||
})
|
||||
|
||||
it('searches for the term when enter is pressed', async () => {
|
||||
select.trigger('input')
|
||||
select.trigger('keyup.enter')
|
||||
await expect(wrapper.emitted().search[0]).toEqual(['abcd'])
|
||||
})
|
||||
|
||||
it('calls onDelete when the delete key is pressed', () => {
|
||||
const spy = jest.spyOn(wrapper.vm, 'onDelete')
|
||||
select.trigger('input')
|
||||
select.trigger('keyup.delete')
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('calls query when a user starts a search by pressing enter', () => {
|
||||
const spy = jest.spyOn(wrapper.vm, 'query')
|
||||
select.trigger('input')
|
||||
select.trigger('keyup.enter')
|
||||
expect(spy).toHaveBeenCalledWith('abcd')
|
||||
})
|
||||
|
||||
it('calls onSelect when a user selects an item in the search dropdown menu', async () => {
|
||||
// searched for term in the browser, copied the results from Vuex in Vue dev tools
|
||||
propsData = {
|
||||
results: [
|
||||
{
|
||||
__typename: 'Post',
|
||||
author: {
|
||||
__typename: 'User',
|
||||
id: 'u5',
|
||||
name: 'Trick',
|
||||
slug: 'trick'
|
||||
},
|
||||
commentsCount: 0,
|
||||
createdAt: '2019-03-13T11:00:20.835Z',
|
||||
id: 'p10',
|
||||
label: 'Eos aut illo omnis quis eaque et iure aut.',
|
||||
shoutedCount: 0,
|
||||
slug: 'eos-aut-illo-omnis-quis-eaque-et-iure-aut',
|
||||
value: 'Eos aut illo omnis quis eaque et iure aut.'
|
||||
}
|
||||
]
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
select.trigger('input')
|
||||
const results = wrapper.find('.ds-select-option')
|
||||
results.trigger('click')
|
||||
await expect(wrapper.emitted().select[0]).toEqual(propsData.results)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
289
components/SearchInput.vue
Normal file
289
components/SearchInput.vue
Normal file
@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<div
|
||||
class="search"
|
||||
aria-label="search"
|
||||
role="search"
|
||||
:class="{
|
||||
'is-active': isActive,
|
||||
'is-open': isOpen
|
||||
}"
|
||||
>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<a
|
||||
v-if="isActive"
|
||||
class="search-clear-btn"
|
||||
@click="clear"
|
||||
>
|
||||
|
||||
</a>
|
||||
<ds-select
|
||||
:id="id"
|
||||
ref="input"
|
||||
v-model="searchValue"
|
||||
class="input"
|
||||
name="search"
|
||||
type="search"
|
||||
icon="search"
|
||||
label-prop="id"
|
||||
:no-options-available="emptyText"
|
||||
:icon-right="isActive ? 'close' : null"
|
||||
:filter="item => item"
|
||||
:options="results"
|
||||
:auto-reset-search="!searchValue"
|
||||
:placeholder="$t('search.placeholder')"
|
||||
:loading="pending"
|
||||
@keyup.enter.native="onEnter"
|
||||
@focus.capture.native="onFocus"
|
||||
@blur.capture.native="onBlur"
|
||||
@keyup.delete.native="onDelete"
|
||||
@keyup.esc.native="clear"
|
||||
@input.exact="onSelect"
|
||||
@input.native="handleInput"
|
||||
@click.capture.native="isOpen = true"
|
||||
>
|
||||
<template
|
||||
slot="option"
|
||||
slot-scope="{option}"
|
||||
>
|
||||
<ds-flex>
|
||||
<ds-flex-item class="search-option-label">
|
||||
<ds-text>
|
||||
{{ option.label | truncate(70) }}
|
||||
</ds-text>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item
|
||||
class="search-option-meta"
|
||||
width="280px"
|
||||
>
|
||||
<ds-flex>
|
||||
<ds-flex-item>
|
||||
<ds-text
|
||||
size="small"
|
||||
color="softer"
|
||||
class="search-meta"
|
||||
>
|
||||
<span style="text-align: right;">
|
||||
<b>{{ option.commentsCount }}</b> <ds-icon name="comments" />
|
||||
</span>
|
||||
<span style="width: 36px; display: inline-block; text-align: right;">
|
||||
<b>{{ option.shoutedCount }}</b> <ds-icon name="bullhorn" />
|
||||
</span>
|
||||
</ds-text>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item>
|
||||
<ds-text
|
||||
size="small"
|
||||
color="softer"
|
||||
align="right"
|
||||
>
|
||||
{{ option.author.name | truncate(32) }} - {{ option.createdAt | dateTime('dd.MM.yyyy') }}
|
||||
</ds-text>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</template>
|
||||
</ds-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'SearchInput',
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: 'nav-search'
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
results: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
delay: {
|
||||
type: Number,
|
||||
default: 300
|
||||
},
|
||||
pending: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchProcess: null,
|
||||
isOpen: false,
|
||||
lastSearchTerm: '',
|
||||
unprocessedSearchInput: '',
|
||||
searchValue: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// #: Unused at the moment?
|
||||
isActive() {
|
||||
return !isEmpty(this.lastSearchTerm)
|
||||
},
|
||||
emptyText() {
|
||||
return this.isActive && !this.pending
|
||||
? this.$t('search.failed')
|
||||
: this.$t('search.hint')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async query(value) {
|
||||
if (isEmpty(value) || value.length < 3) {
|
||||
this.clear()
|
||||
return
|
||||
}
|
||||
this.$emit('search', value)
|
||||
},
|
||||
handleInput(e) {
|
||||
clearTimeout(this.searchProcess)
|
||||
const value = e.target ? e.target.value.trim() : ''
|
||||
this.isOpen = true
|
||||
this.unprocessedSearchInput = value
|
||||
this.searchProcess = setTimeout(() => {
|
||||
this.lastSearchTerm = value
|
||||
this.query(value)
|
||||
}, this.delay)
|
||||
},
|
||||
onSelect(item) {
|
||||
this.isOpen = false
|
||||
this.$emit('select', item)
|
||||
this.$nextTick(() => {
|
||||
this.searchValue = this.lastSearchTerm
|
||||
})
|
||||
},
|
||||
onFocus(e) {
|
||||
clearTimeout(this.searchProcess)
|
||||
this.isOpen = true
|
||||
},
|
||||
onBlur(e) {
|
||||
this.searchValue = this.lastSearchTerm
|
||||
// this.$nextTick(() => {
|
||||
// this.searchValue = this.lastSearchTerm
|
||||
// })
|
||||
this.isOpen = false
|
||||
clearTimeout(this.searchProcess)
|
||||
},
|
||||
onDelete(e) {
|
||||
clearTimeout(this.searchProcess)
|
||||
const value = e.target ? e.target.value.trim() : ''
|
||||
if (isEmpty(value)) {
|
||||
this.clear()
|
||||
} else {
|
||||
this.handleInput(e)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* TODO: on enter we should go to a dedicated seach page!?
|
||||
*/
|
||||
onEnter(e) {
|
||||
// this.isOpen = false
|
||||
clearTimeout(this.searchProcess)
|
||||
if (!this.pending) {
|
||||
// this.lastSearchTerm = this.unprocessedSearchInput
|
||||
this.query(this.unprocessedSearchInput)
|
||||
}
|
||||
},
|
||||
clear() {
|
||||
this.$emit('clear')
|
||||
clearTimeout(this.searchProcess)
|
||||
this.isOpen = false
|
||||
this.unprocessedSearchInput = ''
|
||||
this.lastSearchTerm = ''
|
||||
this.searchValue = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.search {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
$padding-left: $space-x-small;
|
||||
|
||||
.search-option-label {
|
||||
align-self: center;
|
||||
padding-left: $padding-left;
|
||||
}
|
||||
|
||||
.search-option-meta {
|
||||
align-self: center;
|
||||
|
||||
.ds-flex {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
&,
|
||||
.ds-select-dropdown {
|
||||
transition: box-shadow 100ms;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
.ds-select-dropdown {
|
||||
box-shadow: $box-shadow-x-large;
|
||||
}
|
||||
}
|
||||
|
||||
.ds-select-dropdown-message {
|
||||
opacity: 0.5;
|
||||
padding-left: $padding-left;
|
||||
}
|
||||
|
||||
.search-clear-btn {
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 36px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-meta {
|
||||
float: right;
|
||||
padding-top: 2px;
|
||||
white-space: nowrap;
|
||||
word-wrap: none;
|
||||
|
||||
.ds-icon {
|
||||
vertical-align: sub;
|
||||
}
|
||||
}
|
||||
|
||||
.ds-select {
|
||||
z-index: $z-index-dropdown + 1;
|
||||
}
|
||||
|
||||
.ds-select-option-hover {
|
||||
.ds-text-size-small,
|
||||
.ds-text-size-small-x {
|
||||
color: $text-color-soft;
|
||||
}
|
||||
}
|
||||
|
||||
.field {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.control {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
41
cypress/integration/06.Search.feature
Normal file
41
cypress/integration/06.Search.feature
Normal file
@ -0,0 +1,41 @@
|
||||
Feature: Search
|
||||
As a user
|
||||
I would like to be able to search for specific words
|
||||
In order to find related content
|
||||
|
||||
Background:
|
||||
Given I have a user account
|
||||
And we have the following posts in our database:
|
||||
| Author | id | title | content |
|
||||
| Brianna Wiest | p1 | 101 Essays that will change the way you think | 101 Essays, of course! |
|
||||
| Brianna Wiest | p1 | No searched for content | will be found in this post, I guarantee |
|
||||
Given I am logged in
|
||||
|
||||
Scenario: Search for specific words
|
||||
When I search for "Essays"
|
||||
Then I should have one post in the select dropdown
|
||||
Then I should see the following posts in the select dropdown:
|
||||
| title |
|
||||
| 101 Essays that will change the way you think |
|
||||
|
||||
Scenario: Press enter starts search
|
||||
When I type "Essa" and press Enter
|
||||
Then I should have one post in the select dropdown
|
||||
Then I should see the following posts in the select dropdown:
|
||||
| title |
|
||||
| 101 Essays that will change the way you think |
|
||||
|
||||
Scenario: Press escape clears search
|
||||
When I type "Ess" and press escape
|
||||
Then the search field should clear
|
||||
|
||||
Scenario: Select entry goes to post
|
||||
When I search for "Essays"
|
||||
And I select an entry
|
||||
Then I should be on the post's page
|
||||
|
||||
Scenario: Select dropdown content
|
||||
When I search for "Essays"
|
||||
Then I should have one post in the select dropdown
|
||||
Then I should see posts with the searched-for term in the select dropdown
|
||||
And I should not see posts without the searched-for term in the select dropdown
|
||||
69
cypress/integration/common/search.js
Normal file
69
cypress/integration/common/search.js
Normal file
@ -0,0 +1,69 @@
|
||||
import { When, Then } from 'cypress-cucumber-preprocessor/steps'
|
||||
When('I search for {string}', value => {
|
||||
cy.get('#nav-search')
|
||||
.focus()
|
||||
.type(value)
|
||||
})
|
||||
|
||||
Then('I should have one post in the select dropdown', () => {
|
||||
cy.get('.ds-select-dropdown').should($li => {
|
||||
expect($li).to.have.length(1)
|
||||
})
|
||||
})
|
||||
|
||||
Then('I should see the following posts in the select dropdown:', table => {
|
||||
table.hashes().forEach(({ title }) => {
|
||||
cy.get('.ds-select-dropdown').should('contain', title)
|
||||
})
|
||||
})
|
||||
|
||||
When('I type {string} and press Enter', value => {
|
||||
cy.get('#nav-search')
|
||||
.focus()
|
||||
.type(value)
|
||||
.type('{enter}', { force: true })
|
||||
})
|
||||
|
||||
When('I type {string} and press escape', value => {
|
||||
cy.get('#nav-search')
|
||||
.focus()
|
||||
.type(value)
|
||||
.type('{esc}')
|
||||
})
|
||||
|
||||
Then('the search field should clear', () => {
|
||||
cy.get('#nav-search').should('have.text', '')
|
||||
})
|
||||
|
||||
When('I select an entry', () => {
|
||||
cy.get('.ds-select-dropdown ul li')
|
||||
.first()
|
||||
.trigger('click')
|
||||
})
|
||||
|
||||
Then("I should be on the post's page", () => {
|
||||
cy.location('pathname').should(
|
||||
'eq',
|
||||
'/post/101-essays-that-will-change-the-way-you-think/'
|
||||
)
|
||||
})
|
||||
|
||||
Then(
|
||||
'I should see posts with the searched-for term in the select dropdown',
|
||||
() => {
|
||||
cy.get('.ds-select-dropdown').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',
|
||||
() => {
|
||||
cy.get('.ds-select-dropdown').should(
|
||||
'not.contain',
|
||||
'No searched for content'
|
||||
)
|
||||
}
|
||||
)
|
||||
@ -1,21 +1,33 @@
|
||||
<template>
|
||||
<div class="layout-default">
|
||||
<div class="main-navigation">
|
||||
<ds-container style="padding: .5rem 2rem .2rem; display: flex;">
|
||||
<ds-container class="main-navigation-container">
|
||||
<div class="main-navigation-left">
|
||||
<a
|
||||
v-router-link
|
||||
style="display: inline-flex"
|
||||
href="/"
|
||||
>
|
||||
<ds-logo />
|
||||
</a>
|
||||
</div>
|
||||
<div class="main-navigation-center hc-navbar-search">
|
||||
<search-input
|
||||
id="nav-search"
|
||||
:delay="300"
|
||||
:pending="quickSearchPending"
|
||||
:results="quickSearchResults"
|
||||
@clear="quickSearchClear"
|
||||
@search="value => quickSearch({ value })"
|
||||
@select="goToPost"
|
||||
/>
|
||||
</div>
|
||||
<div class="main-navigation-right">
|
||||
<no-ssr>
|
||||
<locale-switch
|
||||
class="topbar-locale-switch"
|
||||
placement="bottom"
|
||||
offset="12"
|
||||
offset="23"
|
||||
/>
|
||||
</no-ssr>
|
||||
<template v-if="isLoggedIn">
|
||||
@ -99,25 +111,35 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import LocaleSwitch from '~/components/LocaleSwitch'
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import SearchInput from '~/components/SearchInput.vue'
|
||||
import Modal from '~/components/Modal'
|
||||
import seo from '~/components/mixins/seo'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Dropdown,
|
||||
LocaleSwitch,
|
||||
SearchInput,
|
||||
Modal,
|
||||
LocaleSwitch
|
||||
},
|
||||
mixins: [seo],
|
||||
data() {
|
||||
return {
|
||||
mobileSearchVisible: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'auth/user',
|
||||
isLoggedIn: 'auth/isLoggedIn',
|
||||
isModerator: 'auth/isModerator',
|
||||
isAdmin: 'auth/isAdmin'
|
||||
isAdmin: 'auth/isAdmin',
|
||||
quickSearchResults: 'search/quickResults',
|
||||
quickSearchPending: 'search/quickPending'
|
||||
}),
|
||||
routes() {
|
||||
if (!this.user.slug) {
|
||||
@ -153,6 +175,18 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
quickSearchClear: 'search/quickClear',
|
||||
quickSearch: 'search/quickSearch'
|
||||
}),
|
||||
goToPost(item) {
|
||||
this.$nextTick(() => {
|
||||
this.$router.push({
|
||||
name: 'post-slug',
|
||||
params: { slug: item.slug }
|
||||
})
|
||||
})
|
||||
},
|
||||
matcher(url, route) {
|
||||
if (url.indexOf('/profile') === 0) {
|
||||
// do only match own profile
|
||||
@ -168,6 +202,8 @@ export default {
|
||||
.topbar-locale-switch {
|
||||
display: flex;
|
||||
margin-right: $space-xx-small;
|
||||
align-self: center;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.main-navigation {
|
||||
@ -176,13 +212,31 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.main-navigation-container {
|
||||
padding: $space-x-small $space-large !important;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.main-navigation-left {
|
||||
display: flex;
|
||||
margin-right: auto;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main-navigation-center {
|
||||
display: flex;
|
||||
flex: auto;
|
||||
width: 100%;
|
||||
padding-right: $space-large;
|
||||
padding-left: $space-large;
|
||||
}
|
||||
|
||||
.main-navigation-right {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.avatar-menu-trigger {
|
||||
|
||||
@ -17,6 +17,11 @@
|
||||
"shouted": "Empfohlen",
|
||||
"commented": "Kommentiert"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Suchen",
|
||||
"hint": "Wonach suchst du?",
|
||||
"failed": "Nichts gefunden"
|
||||
},
|
||||
"settings": {
|
||||
"name": "Einstellungen",
|
||||
"data": {
|
||||
|
||||
@ -17,6 +17,11 @@
|
||||
"shouted": "Shouted",
|
||||
"commented": "Commented"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search",
|
||||
"hint": "What are you searching for?",
|
||||
"failed": "Nothing found"
|
||||
},
|
||||
"settings": {
|
||||
"name": "Settings",
|
||||
"data": {
|
||||
|
||||
@ -12,7 +12,10 @@
|
||||
"generate": "nuxt generate",
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
"test": "jest",
|
||||
"precommit": "yarn lint"
|
||||
"precommit": "yarn lint",
|
||||
"e2e:local": "cypress run --headed",
|
||||
"e2e:ci": "npm-run-all --parallel --race start:ci 'cypress:ci --config baseUrl=http://localhost:3000'",
|
||||
"test:unit:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --no-cache --runInBand"
|
||||
},
|
||||
"cypress-cucumber-preprocessor": {
|
||||
"nonGlobalStepDefinitions": true
|
||||
|
||||
@ -92,7 +92,7 @@ export default {
|
||||
query() {
|
||||
return gql(`
|
||||
query Post($first: Int, $offset: Int) {
|
||||
Post(first: $first, offset: $offset, orderBy: createdAt_desc) {
|
||||
Post(first: $first, offset: $offset) {
|
||||
id
|
||||
title
|
||||
contentExcerpt
|
||||
|
||||
@ -140,7 +140,15 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ready: false
|
||||
post: null,
|
||||
ready: false,
|
||||
title: 'loading'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
Post(post) {
|
||||
this.post = post[0] || {}
|
||||
this.title = this.post.title
|
||||
}
|
||||
},
|
||||
async asyncData(context) {
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
:options="cities"
|
||||
:label="$t('settings.data.labelCity')"
|
||||
:placeholder="$t('settings.data.labelCity')"
|
||||
:loading="loading"
|
||||
@input.native="handleCityInput"
|
||||
/>
|
||||
<!-- eslint-enable vue/use-v-on-exact -->
|
||||
@ -88,6 +89,7 @@ export default {
|
||||
axiosSource: null,
|
||||
cities: [],
|
||||
sending: false,
|
||||
loading: false,
|
||||
formData: {}
|
||||
}
|
||||
},
|
||||
@ -179,6 +181,7 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
this.axiosSource = CancelToken.source()
|
||||
|
||||
const place = encodeURIComponent(value)
|
||||
@ -194,6 +197,9 @@ export default {
|
||||
.then(res => {
|
||||
this.cities = this.processCityResults(res)
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
86
store/search.js
Normal file
86
store/search.js
Normal file
@ -0,0 +1,86 @@
|
||||
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 findPosts($filter: String!) {
|
||||
findPosts(filter: $filter, limit: 10) {
|
||||
id
|
||||
slug
|
||||
label: title
|
||||
value: title,
|
||||
shoutedCount
|
||||
commentsCount
|
||||
createdAt
|
||||
author {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
`),
|
||||
variables: {
|
||||
filter: value.replace(/\s/g, '~ ') + '~'
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
commit('SET_QUICK_RESULTS', res.data.findPosts || [])
|
||||
})
|
||||
.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