[deploy] Execute Reading List searches in Elasticsearch (#7440)
* Execute Reading List searches in Elasticsearch * add additional readinglist front tests
This commit is contained in:
parent
57ae315c31
commit
4ab202ab0a
13 changed files with 101 additions and 233 deletions
|
|
@ -5,7 +5,6 @@
|
|||
//= require utilities/getImageForLink
|
||||
//= require honeybadger-js/dist/honeybadger.js
|
||||
//= require serviceworker-companion
|
||||
//= require algolia/v3/algoliasearch.min
|
||||
|
||||
var instantClick
|
||||
, InstantClick = instantClick = function(document, location, $userAgent) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
/* global algoliasearch, insertAfter, insertArticles, buildArticleHTML, nextPage:writable, fetching:writable, done:writable */
|
||||
/* global insertAfter, insertArticles, buildArticleHTML, nextPage:writable, fetching:writable, done:writable */
|
||||
|
||||
var client;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ class ReadingListItemsController < ApplicationController
|
|||
def index
|
||||
@reading_list_items_index = true
|
||||
set_view
|
||||
generate_algolia_search_key
|
||||
end
|
||||
|
||||
def update
|
||||
|
|
@ -16,18 +15,11 @@ class ReadingListItemsController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def generate_algolia_search_key
|
||||
params = { filters: "viewable_by:#{session_current_user_id}" }
|
||||
@secured_algolia_key = Algolia.generate_secured_api_key(
|
||||
ApplicationConfig["ALGOLIASEARCH_SEARCH_ONLY_KEY"], params
|
||||
)
|
||||
end
|
||||
|
||||
def set_view
|
||||
@view = if params[:view] == "archive"
|
||||
"archived"
|
||||
else
|
||||
"valid"
|
||||
"valid,confirmed"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ module.exports = {
|
|||
InstantClick: false,
|
||||
filterXSS: false,
|
||||
Pusher: false,
|
||||
algoliasearch: false,
|
||||
ga: false,
|
||||
Honeybadger: false,
|
||||
AndroidBridge: false,
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
global.document.head.innerHTML =
|
||||
"<meta name='algolia-public-id' content='abc123' />" +
|
||||
"<meta name='algolia-public-key' content='abc123' />" +
|
||||
"<meta name='environment' content='test' />";
|
||||
|
||||
const mockIndex = {
|
||||
search: (query, _options) =>
|
||||
new Promise((resolve, _reject) => {
|
||||
process.nextTick(() => {
|
||||
const searchResults = {
|
||||
gi: {
|
||||
hits: [
|
||||
{
|
||||
name: 'git',
|
||||
bg_color_hex: '#888751',
|
||||
text_color_hex: '#56c938',
|
||||
hotness_score: 0,
|
||||
supported: true,
|
||||
objectID: '4',
|
||||
_highlightResult: {
|
||||
name: {
|
||||
value: '<em>gi</em>t',
|
||||
matchLevel: 'full',
|
||||
fullyHighlighted: false,
|
||||
matchedWords: ['gi'],
|
||||
},
|
||||
bg_color_hex: {
|
||||
value: '#888751',
|
||||
matchLevel: 'none',
|
||||
matchedWords: [],
|
||||
},
|
||||
text_color_hex: {
|
||||
value: '#56c938',
|
||||
matchLevel: 'none',
|
||||
matchedWords: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
nbHits: 1,
|
||||
page: 0,
|
||||
nbPages: 1,
|
||||
hitsPerPage: 10,
|
||||
processingTimeMS: 1,
|
||||
exhaustiveNbHits: true,
|
||||
query: 'gi',
|
||||
params:
|
||||
'query=gi&hitsPerPage=10&filters=supported%3Atrue&restrictIndices=searchables_development%2CTag_development%2Cordered_articles_development%2Cordered_articles_by_published_at_development%2Cordered_articles_by_positive_reactions_count_development',
|
||||
},
|
||||
};
|
||||
|
||||
const results = searchResults[query] || { hits: [] };
|
||||
|
||||
resolve(results);
|
||||
});
|
||||
}),
|
||||
};
|
||||
const client = {
|
||||
initIndex: _index => mockIndex,
|
||||
};
|
||||
|
||||
export default jest.fn().mockImplementation((_id, _key) => client);
|
||||
|
|
@ -16,7 +16,7 @@ import { ItemListItemArchiveButton } from '../src/components/ItemList/ItemListIt
|
|||
import { ItemListLoadMoreButton } from '../src/components/ItemList/ItemListLoadMoreButton';
|
||||
import { ItemListTags } from '../src/components/ItemList/ItemListTags';
|
||||
|
||||
const STATUS_VIEW_VALID = 'valid';
|
||||
const STATUS_VIEW_VALID = 'valid,confirmed';
|
||||
const STATUS_VIEW_ARCHIVED = 'archived';
|
||||
const READING_LIST_ARCHIVE_PATH = '/readinglist/archive';
|
||||
const READING_LIST_PATH = '/readinglist';
|
||||
|
|
@ -50,19 +50,14 @@ export class ReadingList extends Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { hitsPerPage, statusView } = this.state;
|
||||
const { statusView } = this.state;
|
||||
|
||||
this.performInitialSearch({
|
||||
containerId: 'reading-list',
|
||||
indexName: 'SecuredReactions',
|
||||
searchOptions: {
|
||||
hitsPerPage,
|
||||
filters: `status:${statusView}`,
|
||||
},
|
||||
searchOptions: { status: `${statusView}` },
|
||||
});
|
||||
}
|
||||
|
||||
toggleStatusView = event => {
|
||||
toggleStatusView = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const { query, selectedTags } = this.state;
|
||||
|
|
@ -174,12 +169,12 @@ export class ReadingList extends Component {
|
|||
const isStatusViewValid = this.statusViewValid();
|
||||
|
||||
const archiveButtonLabel = isStatusViewValid ? 'archive' : 'unarchive';
|
||||
const itemsToRender = items.map(item => {
|
||||
const itemsToRender = items.map((item) => {
|
||||
return (
|
||||
<ItemListItem item={item}>
|
||||
<ItemListItemArchiveButton
|
||||
text={archiveButtonLabel}
|
||||
onClick={e => this.toggleArchiveStatus(e, item)}
|
||||
onClick={(e) => this.toggleArchiveStatus(e, item)}
|
||||
/>
|
||||
</ItemListItem>
|
||||
);
|
||||
|
|
@ -226,7 +221,7 @@ export class ReadingList extends Component {
|
|||
<div className="status-view-toggle">
|
||||
<a
|
||||
href={READING_LIST_ARCHIVE_PATH}
|
||||
onClick={e => this.toggleStatusView(e)}
|
||||
onClick={(e) => this.toggleStatusView(e)}
|
||||
data-no-instant
|
||||
>
|
||||
{isStatusViewValid ? 'View Archive' : 'View Reading List'}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Shared behavior between the reading list and history pages
|
||||
import setupAlgoliaIndex from '../src/utils/algolia';
|
||||
import { fetchSearch } from '../src/utils/search';
|
||||
|
||||
// Provides the initial state for the component
|
||||
export function defaultState(options) {
|
||||
|
|
@ -59,25 +59,24 @@ export function clearSelectedTags(event) {
|
|||
}
|
||||
|
||||
// Perform the initial search
|
||||
export function performInitialSearch({
|
||||
containerId,
|
||||
indexName,
|
||||
searchOptions = {},
|
||||
}) {
|
||||
export function performInitialSearch({ searchOptions = {} }) {
|
||||
const component = this;
|
||||
const { hitsPerPage } = component.state;
|
||||
const dataHash = { page: 0, per_page: hitsPerPage };
|
||||
|
||||
const index = setupAlgoliaIndex({ containerId, indexName });
|
||||
if (searchOptions.status) {
|
||||
dataHash.status = searchOptions.status.split(',');
|
||||
}
|
||||
|
||||
index.search('', searchOptions).then(result => {
|
||||
const responsePromise = fetchSearch('reactions', dataHash);
|
||||
return responsePromise.then((response) => {
|
||||
const reactions = response.result;
|
||||
component.setState({
|
||||
items: result.hits,
|
||||
totalCount: result.nbHits,
|
||||
index, // set the index in the component state, to be retrieved later
|
||||
page: 0,
|
||||
items: reactions,
|
||||
itemsLoaded: true,
|
||||
// show the button if the number of total results is greater
|
||||
// than the number of results for the current page
|
||||
showLoadMoreButton: result.nbHits > hitsPerPage,
|
||||
totalCount: response.total,
|
||||
showLoadMoreButton: hitsPerPage < response.total,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -90,28 +89,31 @@ export function search(query, { page, tags, statusView }) {
|
|||
// we check `undefined` because page can be 0
|
||||
const newPage = page === undefined ? component.state.page : page;
|
||||
|
||||
const { index, hitsPerPage, items } = component.state;
|
||||
const { hitsPerPage } = component.state;
|
||||
const dataHash = {
|
||||
search_fields: query,
|
||||
page: newPage,
|
||||
per_page: hitsPerPage,
|
||||
};
|
||||
|
||||
const filters = { hitsPerPage, page: newPage };
|
||||
if (tags && tags.length > 0) {
|
||||
filters.tagFilters = tags;
|
||||
dataHash.tag_names = tags;
|
||||
dataHash.tag_boolean_mode = 'all';
|
||||
}
|
||||
|
||||
if (statusView) {
|
||||
filters.filters = `status:${statusView}`;
|
||||
dataHash.status = statusView.split(',');
|
||||
}
|
||||
index.search(query, filters).then(result => {
|
||||
// append new items at the end
|
||||
const allItems =
|
||||
page === undefined ? result.hits : [...items, ...result.hits];
|
||||
|
||||
const responsePromise = fetchSearch('reactions', dataHash);
|
||||
return responsePromise.then((response) => {
|
||||
const reactions = response.result;
|
||||
component.setState({
|
||||
query,
|
||||
page: newPage,
|
||||
items: result.hits,
|
||||
totalCount: allItems.length,
|
||||
// show the button if the number of items is lower than the number
|
||||
// of available results
|
||||
showLoadMoreButton: allItems.length < result.nbHits,
|
||||
items: reactions,
|
||||
totalCount: response.total,
|
||||
showLoadMoreButton: reactions.length < response.total,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ import { PropTypes } from 'preact-compat';
|
|||
|
||||
export const ItemListItem = ({ item, children }) => {
|
||||
const adaptedItem = {
|
||||
path: item.article_path || item.searchable_reactable_path,
|
||||
title: item.article_title || item.searchable_reactable_title,
|
||||
user: item.article_user || item.reactable_user,
|
||||
publishedDate: item.reactable_published_date,
|
||||
visitedDate: item.readable_visited_at,
|
||||
readingTime: item.article_reading_time || item.reading_time,
|
||||
tags: item.article_tags || item.reactable_tags,
|
||||
path: item.reactable.path,
|
||||
title: item.reactable.title,
|
||||
user: item.reactable.user,
|
||||
publishedDate: item.reactable.published_date_string,
|
||||
readingTime: item.reactable.reading_time,
|
||||
tags: item.reactable.tags,
|
||||
};
|
||||
|
||||
// update readingTime to 1 min if the reading time is less than 1 min
|
||||
|
|
@ -24,17 +23,15 @@ export const ItemListItem = ({ item, children }) => {
|
|||
<a className="item-user" href={`/${adaptedItem.user.username}`}>
|
||||
<img src={adaptedItem.user.profile_image_90} alt="Profile Pic" />
|
||||
{`${adaptedItem.user.name}・`}
|
||||
{adaptedItem.visitedDate
|
||||
? `visited on ${adaptedItem.visitedDate}・`
|
||||
: `${adaptedItem.publishedDate}・`}
|
||||
{`${adaptedItem.publishedDate}・`}
|
||||
{`${adaptedItem.readingTime} min read・`}
|
||||
</a>
|
||||
|
||||
{adaptedItem.tags.length > 0 ? (
|
||||
<span className="item-tags">
|
||||
{adaptedItem.tags.map(tag => (
|
||||
<a className="item-tag" href={`/t/${tag}`}>
|
||||
{`#${tag}`}
|
||||
{adaptedItem.tags.map((tag) => (
|
||||
<a className="item-tag" href={`/t/${tag.name}`}>
|
||||
{`#${tag.name}`}
|
||||
</a>
|
||||
))}
|
||||
</span>
|
||||
|
|
@ -49,30 +46,19 @@ export const ItemListItem = ({ item, children }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const historyItemPropTypes = PropTypes.shape({
|
||||
article_path: PropTypes.string.isRequired,
|
||||
article_title: PropTypes.string.isRequired,
|
||||
article_user: PropTypes.shape({
|
||||
username: PropTypes.string.isRequired,
|
||||
profile_image_90: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
}),
|
||||
article_reading_time: PropTypes.number.isRequired,
|
||||
readable_visited_at: PropTypes.string.isRequired,
|
||||
article_tags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
});
|
||||
|
||||
const readingListItemPropTypes = PropTypes.shape({
|
||||
searchable_reactable_path: PropTypes.string.isRequired,
|
||||
searchable_reactable_title: PropTypes.string.isRequired,
|
||||
reactable_user: PropTypes.shape({
|
||||
username: PropTypes.string.isRequired,
|
||||
profile_image_90: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
}),
|
||||
reading_time: PropTypes.number.isRequired,
|
||||
reactable_published_date: PropTypes.string.isRequired,
|
||||
reactable_tags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
reactable: {
|
||||
path: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
reading_time: PropTypes.number.isRequired,
|
||||
published_date_string: PropTypes.string.isRequired,
|
||||
user: PropTypes.shape({
|
||||
username: PropTypes.string.isRequired,
|
||||
profile_image_90: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
}),
|
||||
tags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
},
|
||||
});
|
||||
|
||||
ItemListItem.defaultProps = {
|
||||
|
|
@ -80,7 +66,6 @@ ItemListItem.defaultProps = {
|
|||
};
|
||||
|
||||
ItemListItem.propTypes = {
|
||||
item: PropTypes.oneOfType([historyItemPropTypes, readingListItemPropTypes])
|
||||
.isRequired,
|
||||
item: PropTypes.oneOfType([readingListItemPropTypes]).isRequired,
|
||||
children: PropTypes.element,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,55 +3,61 @@ import render from 'preact-render-to-json';
|
|||
import { shallow } from 'preact-render-spy';
|
||||
import { ItemListItem } from '../ItemListItem';
|
||||
|
||||
const historyItem = {
|
||||
article_path: '/article',
|
||||
article_title: 'Title',
|
||||
article_user: {
|
||||
username: 'bob',
|
||||
profile_image_90: 'https://dummyimage.com/90x90',
|
||||
name: 'Bob',
|
||||
},
|
||||
article_reading_time: 1,
|
||||
readable_visited_at: 'Jun 29',
|
||||
article_tags: ['discuss'],
|
||||
};
|
||||
|
||||
const item = {
|
||||
searchable_reactable_path: '/article',
|
||||
searchable_reactable_title: 'Title',
|
||||
reactable_user: {
|
||||
username: 'bob',
|
||||
profile_image_90: 'https://dummyimage.com/90x90',
|
||||
name: 'Bob',
|
||||
reactable: {
|
||||
path: '/article',
|
||||
title: 'Title',
|
||||
published_date_string: 'Jun 29',
|
||||
reading_time: 1,
|
||||
user: {
|
||||
username: 'bob',
|
||||
profile_image_90: 'https://dummyimage.com/90x90',
|
||||
name: 'Bob',
|
||||
},
|
||||
tags: [{ name: 'discuss' }],
|
||||
},
|
||||
reading_time: 1,
|
||||
reactable_published_date: 'Jun 29',
|
||||
reactable_tags: ['discuss'],
|
||||
};
|
||||
|
||||
describe('<ItemListItem />', () => {
|
||||
it('renders properly with a history item', () => {
|
||||
const tree = render(<ItemListItem item={historyItem} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders properly with a readinglist item', () => {
|
||||
const tree = render(<ItemListItem item={item} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders with readingtime of 1 min if reading time is less than 1 min.', () => {
|
||||
const wrapper = shallow(<ItemListItem item={{...item, reading_time: 0.5 }} />);
|
||||
item.reactable.reading_time = 0.5;
|
||||
const wrapper = shallow(<ItemListItem item={item} />);
|
||||
expect(wrapper.find('.item-user').text()).toContain('1 min read');
|
||||
});
|
||||
|
||||
it('renders with readingtime of 1 min if reading time is null.', () => {
|
||||
const wrapper = shallow(<ItemListItem item={{...item, reading_time: null }} />);
|
||||
item.reactable.reading_time = null;
|
||||
const wrapper = shallow(<ItemListItem item={item} />);
|
||||
expect(wrapper.find('.item-user').text()).toContain('1 min read');
|
||||
});
|
||||
|
||||
it('renders correct readingtime.', () => {
|
||||
item.reactable.reading_time = 10;
|
||||
const wrapper = shallow(<ItemListItem item={item} />);
|
||||
expect(wrapper.find('.item-user').text()).toContain('10 min read');
|
||||
});
|
||||
|
||||
it('renders without any tags if the tags array is empty.', () => {
|
||||
const wrapper = shallow(<ItemListItem item={{...item, reactable_tags: [] }} />);
|
||||
expect(wrapper.find('.item-user').text()).toContain('1 min read');
|
||||
item.reactable.tags = [];
|
||||
const wrapper = shallow(<ItemListItem item={item} />);
|
||||
expect(wrapper.find('.item-tags').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
it('renders tags with links if present.', () => {
|
||||
item.reactable.tags = [{ name: 'discuss' }];
|
||||
const wrapper = shallow(<ItemListItem item={item} />);
|
||||
expect(wrapper.find('.item-tag')[0].attributes.href).toEqual('/t/discuss');
|
||||
expect(wrapper.find('.item-tag').text()).toContain('discuss');
|
||||
});
|
||||
|
||||
it('renders user information', () => {
|
||||
const wrapper = shallow(<ItemListItem item={item} />);
|
||||
expect(wrapper.find('.item-user')[0].attributes.href).toEqual('/bob');
|
||||
expect(wrapper.find('.item-user').text()).toContain('Bob');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,46 +1,5 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ItemListItem /> renders properly with a history item 1`] = `
|
||||
<div
|
||||
class="item-wrapper"
|
||||
>
|
||||
<a
|
||||
class="item"
|
||||
href="/article"
|
||||
>
|
||||
<div
|
||||
class="item-title"
|
||||
>
|
||||
Title
|
||||
</div>
|
||||
<div
|
||||
class="item-details"
|
||||
>
|
||||
<a
|
||||
class="item-user"
|
||||
href="/bob"
|
||||
>
|
||||
<img
|
||||
alt="Profile Pic"
|
||||
src="https://dummyimage.com/90x90"
|
||||
/>
|
||||
Bob・visited on Jun 29・1 min read・
|
||||
</a>
|
||||
<span
|
||||
class="item-tags"
|
||||
>
|
||||
<a
|
||||
class="item-tag"
|
||||
href="/t/discuss"
|
||||
>
|
||||
#discuss
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<ItemListItem /> renders properly with a readinglist item 1`] = `
|
||||
<div
|
||||
class="item-wrapper"
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
export default function setupAlgoliaIndex({ containerId, indexName }) {
|
||||
const id = document.querySelector("meta[name='algolia-public-id']").content;
|
||||
const key = document.getElementById(containerId).dataset.algoliaKey;
|
||||
const env = document.querySelector("meta[name='environment']").content;
|
||||
const client = algoliasearch(id, key);
|
||||
return client.initIndex(`${indexName}_${env}`);
|
||||
}
|
||||
|
|
@ -61,12 +61,12 @@ module Search
|
|||
end
|
||||
|
||||
def terms_keys
|
||||
TERM_KEYS.map do |term_key, search_key|
|
||||
TERM_KEYS.flat_map do |term_key, search_key|
|
||||
next unless @params.key? term_key
|
||||
|
||||
values = Array.wrap(@params[term_key])
|
||||
|
||||
if params[:tag_boolean_mode] == "all"
|
||||
if params[:tag_boolean_mode] == "all" && term_key == :tag_names
|
||||
values.map { |val| { term: { search_key => val } } }
|
||||
else
|
||||
{ terms: { search_key => values } }
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ RSpec.describe Search::Reaction, type: :service do
|
|||
it "filters by status" do
|
||||
reaction1.update(status: "invalid")
|
||||
index_documents([reaction1, reaction2])
|
||||
query_params[:status] = ["valid"]
|
||||
query_params[:status] = %w[valid confirmed]
|
||||
|
||||
reaction_docs = described_class.search_documents(params: query_params)["reactions"]
|
||||
expect(reaction_docs.count).to eq(1)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue