mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
Basic post/user search
refs #5343, #5652 - implements basic post and user search using selectize input - queries minimal API endpoint and refreshes results on search input focus if results are older than 60 seconds
This commit is contained in:
parent
720c421e8b
commit
871ba32343
8 changed files with 281 additions and 8 deletions
147
core/client/app/components/gh-search-input.js
Normal file
147
core/client/app/components/gh-search-input.js
Normal file
|
@ -0,0 +1,147 @@
|
|||
import Ember from 'ember';
|
||||
import {request as ajax} from 'ic-ajax';
|
||||
/* global key */
|
||||
|
||||
export default Ember.Component.extend({
|
||||
|
||||
selection: null,
|
||||
content: [],
|
||||
isLoading: false,
|
||||
contentExpiry: 60 * 1000,
|
||||
contentExpiresAt: false,
|
||||
|
||||
posts: Ember.computed.filterBy('content', 'category', 'Posts'),
|
||||
pages: Ember.computed.filterBy('content', 'category', 'Pages'),
|
||||
users: Ember.computed.filterBy('content', 'category', 'Users'),
|
||||
|
||||
_store: Ember.inject.service('store'),
|
||||
_routing: Ember.inject.service('-routing'),
|
||||
_selectize: Ember.computed(function () {
|
||||
return this.$('select')[0].selectize;
|
||||
}),
|
||||
|
||||
refreshContent: function () {
|
||||
var promises = [],
|
||||
now = new Date(),
|
||||
contentExpiry = this.get('contentExpiry'),
|
||||
contentExpiresAt = this.get('contentExpiresAt'),
|
||||
self = this;
|
||||
|
||||
if (self.get('isLoading') || contentExpiresAt > now) { return; }
|
||||
|
||||
self.set('isLoading', true);
|
||||
promises.pushObject(this._loadPosts());
|
||||
promises.pushObject(this._loadUsers());
|
||||
|
||||
Ember.RSVP.all(promises).then(function () { }).finally(function () {
|
||||
self.set('isLoading', false);
|
||||
self.set('contentExpiresAt', new Date(now.getTime() + contentExpiry));
|
||||
});
|
||||
},
|
||||
|
||||
_loadPosts: function () {
|
||||
var store = this.get('_store'),
|
||||
postsUrl = store.adapterFor('post').urlForFindQuery({}, 'post') + '/',
|
||||
postsQuery = {fields: 'id,title,page', limit: 'all', status: 'all', staticPages: 'all'},
|
||||
content = this.get('content'),
|
||||
self = this;
|
||||
|
||||
return ajax(postsUrl, {data: postsQuery}).then(function (posts) {
|
||||
content.removeObjects(self.get('posts'));
|
||||
content.removeObjects(self.get('pages'));
|
||||
content.pushObjects(posts.posts.map(function (post) {
|
||||
return {
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
category: post.page ? 'Pages' : 'Posts'
|
||||
};
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
_loadUsers: function () {
|
||||
var store = this.get('_store'),
|
||||
usersUrl = store.adapterFor('user').urlForFindQuery({}, 'user') + '/',
|
||||
usersQuery = {fields: 'name,slug', limit: 'all'},
|
||||
content = this.get('content'),
|
||||
self = this;
|
||||
|
||||
return ajax(usersUrl, {data: usersQuery}).then(function (users) {
|
||||
content.removeObjects(self.get('users'));
|
||||
content.pushObjects(users.users.map(function (user) {
|
||||
return {
|
||||
id: user.slug,
|
||||
title: user.name,
|
||||
category: 'Users'
|
||||
};
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
_setKeymasterScope: function () {
|
||||
key.setScope('search-input');
|
||||
},
|
||||
|
||||
_resetKeymasterScope: function () {
|
||||
key.setScope('default');
|
||||
},
|
||||
|
||||
willDestroy: function () {
|
||||
this._resetKeymasterScope();
|
||||
},
|
||||
|
||||
actions: {
|
||||
openSelected: function (selected) {
|
||||
var transition = null,
|
||||
self = this;
|
||||
|
||||
if (!selected) { return; }
|
||||
|
||||
if (selected.category === 'Posts' || selected.category === 'Pages') {
|
||||
transition = self.get('_routing.router').transitionTo('editor.edit', selected.id);
|
||||
}
|
||||
|
||||
if (selected.category === 'Users') {
|
||||
transition = self.get('_routing.router').transitionTo('team.user', selected.id);
|
||||
}
|
||||
|
||||
self.set('selection', '');
|
||||
transition.then(function () {
|
||||
if (self.get('_selectize').$control_input.is(':focus')) {
|
||||
self._setKeymasterScope();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
focusInput: function () {
|
||||
this.get('_selectize').focus();
|
||||
},
|
||||
|
||||
onFocus: function () {
|
||||
this._setKeymasterScope();
|
||||
this.refreshContent();
|
||||
},
|
||||
|
||||
onBlur: function () {
|
||||
this._resetKeymasterScope();
|
||||
},
|
||||
|
||||
// hacky method of disabling the dropdown until a user has typed something
|
||||
// TODO: move into a selectize plugin
|
||||
onInit: function () {
|
||||
var selectize = this.get('_selectize');
|
||||
selectize.on('dropdown_open', function () {
|
||||
if (Ember.isBlank(selectize.$control_input.val())) {
|
||||
selectize.close();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onUpdateFilter: function (filter) {
|
||||
if (Ember.isBlank(filter)) {
|
||||
this.get('_selectize').close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -104,20 +104,26 @@
|
|||
margin: 0 15px 10px;
|
||||
}
|
||||
|
||||
.gh-nav-search-input {
|
||||
padding: 5px 8px;
|
||||
border-radius: 4px;
|
||||
.gh-nav-search-input .selectize-input {
|
||||
padding: 4px 8px;
|
||||
padding-right: 30px;
|
||||
height: auto;
|
||||
}
|
||||
.gh-nav-search-input .selectize-input,
|
||||
.gh-nav-search-input .selectize-input input,
|
||||
.gh-nav-search-input .selectize-dropdown {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.gh-nav-search-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: -5px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 6px 0 5px;
|
||||
padding: 0 10px 0 5px;
|
||||
}
|
||||
|
||||
.gh-nav-search-button i {
|
||||
|
|
|
@ -109,7 +109,7 @@ input {
|
|||
.gh-select,
|
||||
select {
|
||||
display: block;
|
||||
padding: 8px 10px;
|
||||
padding: 4px 10px;
|
||||
width: 100%;
|
||||
border: 1px solid #dfe1e3;
|
||||
border-radius: var(--border-radius);
|
||||
|
|
|
@ -16,8 +16,7 @@
|
|||
{{/gh-dropdown}}
|
||||
<section class="gh-nav-body">
|
||||
<section class="gh-nav-search">
|
||||
<input class="gh-nav-search-input gh-input" type="text" placeholder="Search">
|
||||
<button class="gh-nav-search-button"><i class="icon-search"></i><span class="sr-only">Search</span></button>
|
||||
{{gh-search-input class="gh-nav-search-input"}}
|
||||
</section>
|
||||
<ul class="gh-nav-list gh-nav-main">
|
||||
{{!<li><i class="icon-dash"></i>Dashboard</li>}}
|
||||
|
|
16
core/client/app/templates/components/gh-search-input.hbs
Normal file
16
core/client/app/templates/components/gh-search-input.hbs
Normal file
|
@ -0,0 +1,16 @@
|
|||
{{gh-selectize
|
||||
placeholder="Search"
|
||||
selection=selection
|
||||
content=content
|
||||
loading=isLoading
|
||||
optionValuePath="content.id"
|
||||
optionLabelPath="content.title"
|
||||
optionGroupPath="content.category"
|
||||
openOnFocus=false
|
||||
maxItems="1"
|
||||
on-init="onInit"
|
||||
on-focus="onFocus"
|
||||
on-blur="onBlur"
|
||||
select-item="openSelected"
|
||||
update-filter="onUpdateFilter"}}
|
||||
<button class="gh-nav-search-button" {{action "focusInput"}}><i class="icon-search"></i><span class="sr-only">Search</span></button>
|
25
core/client/tests/unit/components/gh-search-input-test.js
Normal file
25
core/client/tests/unit/components/gh-search-input-test.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describeComponent,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
|
||||
describeComponent(
|
||||
'gh-search-input',
|
||||
'GhSearchInputComponent',
|
||||
{
|
||||
needs: ['component:gh-selectize']
|
||||
},
|
||||
function () {
|
||||
it('renders', function () {
|
||||
// creates the component instance
|
||||
var component = this.subject();
|
||||
expect(component._state).to.equal('preRender');
|
||||
|
||||
// renders the component on the page
|
||||
this.render();
|
||||
expect(component._state).to.equal('inDOM');
|
||||
});
|
||||
}
|
||||
);
|
|
@ -54,6 +54,7 @@ var DEBUG = false, // TOGGLE THIS TO GET MORE SCREENSHOTS
|
|||
},
|
||||
screens,
|
||||
CasperTest,
|
||||
utils = require('utils'),
|
||||
// ## Debugging
|
||||
jsErrors = [],
|
||||
pageErrors = [],
|
||||
|
@ -257,6 +258,22 @@ casper.echoConcise = function (message, style) {
|
|||
}
|
||||
};
|
||||
|
||||
// ### Wait for Selector Text
|
||||
// Does casper.waitForSelector but checks for the presence of specified text
|
||||
// http://stackoverflow.com/questions/32104784/wait-for-an-element-to-have-a-specific-text-with-casperjs
|
||||
casper.waitForSelectorText = function (selector, text, then, onTimeout, timeout) {
|
||||
this.waitForSelector(selector, function _then() {
|
||||
this.waitFor(function _check() {
|
||||
var content = this.fetchText(selector);
|
||||
if (utils.isRegExp(text)) {
|
||||
return text.test(content);
|
||||
}
|
||||
return content.indexOf(text) !== -1;
|
||||
}, then, onTimeout, timeout);
|
||||
}, onTimeout, timeout);
|
||||
return this;
|
||||
};
|
||||
|
||||
// pass through all console.logs
|
||||
casper.on('remote.message', function (msg) {
|
||||
casper.echoConcise('CONSOLE LOG: ' + msg, 'INFO');
|
||||
|
|
|
@ -165,3 +165,66 @@ CasperTest.begin('Can transition to the editor and back', 6, function suite(test
|
|||
test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL');
|
||||
});
|
||||
});
|
||||
|
||||
CasperTest.begin('Can search for posts and users', 8, function suite(test) {
|
||||
var searchControl = '.gh-nav-search-input',
|
||||
searchInput = '.gh-nav-search-input .selectize-input',
|
||||
mouse = require('mouse').create(casper);
|
||||
|
||||
casper.thenOpenAndWaitForPageLoad('root', function testTitleAndUrl() {
|
||||
test.assertTitle('Content - Test Blog', 'Ghost admin has incorrect title');
|
||||
test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL');
|
||||
});
|
||||
|
||||
casper.thenClick('.gh-nav-search-button');
|
||||
|
||||
casper.waitForResource(/posts\/\?fields=id%2Ctitle%2Cpage&limit=all&status=all&staticPages=all/, function then() {
|
||||
test.assert(true, 'Queried filtered posts list on search focus');
|
||||
}, function timeout() {
|
||||
casper.test.fail('Did not query filtered posts list on search focus');
|
||||
});
|
||||
|
||||
casper.waitForResource(/users\/\?fields=name%2Cslug&limit=all/, function then() {
|
||||
test.assert(true, 'Queried filtered users list on search focus');
|
||||
}, function timeout() {
|
||||
casper.test.fail('Did not query filtered users list on search focus');
|
||||
});
|
||||
|
||||
casper.then(function testUserResults() {
|
||||
casper.sendKeys(searchInput, 'Test', {keepFocus: true});
|
||||
casper.waitForSelectorText(searchControl + ' .option.active', 'Test User', function success() {
|
||||
test.assert(true, 'Queried user was displayed when searching');
|
||||
}, function timeout() {
|
||||
casper.test.fail('Queried user was not displayed when searching');
|
||||
});
|
||||
});
|
||||
|
||||
casper.then(function testUserNavigation() {
|
||||
casper.sendKeys(searchInput, casper.page.event.key.Enter, {keepFocus: true});
|
||||
casper.waitForSelector('.settings-user', function () {
|
||||
test.assertUrlMatch(/ghost\/team\/test\//, 'Landed on correct URL');
|
||||
});
|
||||
});
|
||||
|
||||
// casper loses the focus somehow, click off/on the input to regain it
|
||||
casper.thenClick('.gh-input.user-name');
|
||||
casper.thenClick(searchControl + ' .selectize-input');
|
||||
|
||||
casper.wait(500);
|
||||
|
||||
casper.then(function testPostResultsAndClick() {
|
||||
casper.sendKeys(searchInput, 'Welcome', {keepFocus: true});
|
||||
casper.wait(500);
|
||||
casper.then(function () {
|
||||
casper.waitForSelectorText(searchControl + ' .option.active', 'Welcome to Ghost', function success() {
|
||||
test.assert(true, 'Queried post was displayed when searching');
|
||||
mouse.down(searchControl + ' .option.active');
|
||||
casper.waitForSelector('.view-editor', function () {
|
||||
test.assertUrlMatch(/ghost\/editor\/\d\//, 'Landed on correct URL');
|
||||
});
|
||||
}, function timeout() {
|
||||
casper.test.fail('Queried post was not displayed when searching');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue