diff --git a/addon/PlacesProvider.js b/addon/PlacesProvider.js index a440f5bd0b..fb0ddfa3ae 100644 --- a/addon/PlacesProvider.js +++ b/addon/PlacesProvider.js @@ -335,7 +335,7 @@ Links.prototype = { * @returns {Promise} Returns a promise with the array of links as payload. */ _getHistoryLinks: Task.async(function*(options = {}) { - let {limit, afterDate, beforeDate, order, ignoreBlocked} = options; + let {limit, afterDate, beforeDate, filter, order, ignoreBlocked} = options; if (!limit || limit.options > LINKS_QUERY_LIMIT) { limit = LINKS_QUERY_LIMIT; } @@ -359,6 +359,22 @@ Links.prototype = { params.beforeDate = beforeDate; } + // setup filter binding and sql clause + let filterClause = ""; + if (filter) { + // Match each token in either the title or url + let tokens = filter.trim().toLowerCase().split(/\s+/); + tokens.forEach((token, i) => { + filterClause = `${filterClause} AND ( + moz_places.title LIKE :filter${i} OR + moz_places.url LIKE :filter${i})`; + params[`filter${i}`] = `%${token}%`; + }); + } + else { + filter = ""; + } + // setup order by clause let orderbyClause = (order === "bookmarksFrecency") ? "ORDER BY bookmarkDateCreated DESC, frecency DESC, lastVisitDate DESC, url" : @@ -384,6 +400,7 @@ Links.prototype = { AND moz_places.url NOT IN (${blockedURLs}) ${afterDateClause} ${beforeDateClause} + ${filterClause} ${orderbyClause} LIMIT :limit`; @@ -393,6 +410,9 @@ Links.prototype = { params }); + // Remember how these results were filtered + links = links.map(link => Object.assign(link, {filter})); + links = this._faviconBytesToDataURI(links); return links.filter(link => LinkChecker.checkLoadURI(link.url)); }), diff --git a/common/action-manager.js b/common/action-manager.js index 929724e208..bde1587a6e 100644 --- a/common/action-manager.js +++ b/common/action-manager.js @@ -140,9 +140,9 @@ function RequestRecentLinks(options) { return RequestExpect("RECENT_LINKS_REQUEST", "RECENT_LINKS_RESPONSE", options); } -function RequestMoreRecentLinks(beforeDate) { +function RequestMoreRecentLinks(beforeDate, filter = "") { return RequestRecentLinks({ - data: {beforeDate}, + data: {beforeDate, filter}, append: true, meta: {skipPreviewRequest: true} }); diff --git a/common/reducers/Filter.js b/common/reducers/Filter.js index 70c0c0b0de..c7673bb16e 100644 --- a/common/reducers/Filter.js +++ b/common/reducers/Filter.js @@ -10,7 +10,7 @@ module.exports = function Filter(prevState = INITIAL_STATE, action) { const state = Object.assign({}, prevState); switch (action.type) { case am.type("NOTIFY_FILTER_QUERY"): - state.query = action.data; + state.query = action.data || ""; break; } return state; diff --git a/common/reducers/SetRowsOrError.js b/common/reducers/SetRowsOrError.js index e061287ca6..fd0e4f1e00 100644 --- a/common/reducers/SetRowsOrError.js +++ b/common/reducers/SetRowsOrError.js @@ -66,6 +66,10 @@ module.exports = function setRowsOrError(requestType, responseType, querySize) { case am.type("NOTIFY_HISTORY_DELETE"): state.rows = prevState.rows.filter(val => val.url !== action.data); break; + case requestType === "RECENT_LINKS_REQUEST" && am.type("NOTIFY_FILTER_QUERY"): + // Allow loading more even if we hit the end as the filter has changed + state.canLoadMore = true; + break; default: return prevState; } diff --git a/content-src/components/Header/Header.scss b/content-src/components/Header/Header.scss index 3d566f62e8..524036407c 100644 --- a/content-src/components/Header/Header.scss +++ b/content-src/components/Header/Header.scss @@ -49,9 +49,6 @@ background: $search-glyph-image no-repeat 8px center / $search-glyph-size; margin-left: $header-section-spacing; max-width: $timeline-max-width; - - // GH#1305: Temporarily disable for current release - display: none; } .icon-dismiss { diff --git a/content-src/components/TimelinePage/TimelineFeed.js b/content-src/components/TimelinePage/TimelineFeed.js index 30f86a536a..e6b20a6826 100644 --- a/content-src/components/TimelinePage/TimelineFeed.js +++ b/content-src/components/TimelinePage/TimelineFeed.js @@ -10,13 +10,16 @@ const {INFINITE_SCROLL_THRESHOLD, SCROLL_TOP_OFFSET} = require("common/constants const debounce = require("lodash.debounce"); const TimelineFeed = React.createClass({ + getFilterQuery() { + return (this.props.Filter || {}).query || ""; + }, loadMore() { const items = this.props.Feed.rows; if (!items.length) { return; } const beforeDate = items[items.length - 1][this.props.dateKey]; - this.props.dispatch(this.props.loadMoreAction(beforeDate)); + this.props.dispatch(this.props.loadMoreAction(beforeDate, this.getFilterQuery())); this.props.dispatch(NotifyEvent({ event: "LOAD_MORE_SCROLL", page: this.props.pageName, @@ -75,7 +78,7 @@ const TimelineFeed = React.createClass({ }, render() { const props = this.props; - const query = (props.Filter && props.Filter.query) || ""; + const query = this.getFilterQuery(); const showSpotlight = props.Spotlight && query === ""; return (
diff --git a/content-src/selectors/selectors.js b/content-src/selectors/selectors.js index fac053373b..e2515a8110 100644 --- a/content-src/selectors/selectors.js +++ b/content-src/selectors/selectors.js @@ -164,7 +164,10 @@ module.exports.selectHistory = createSelector( return { Spotlight: Object.assign({}, Spotlight, {rows}), Filter, - History + History: Object.assign({}, History, { + // Only include rows that are less filtered than the current filter + rows: History.rows.filter(val => Filter.query.indexOf(val.filter) === 0) + }) }; } ); diff --git a/content-test/selectors/selectors.test.js b/content-test/selectors/selectors.test.js index d293de9905..ea255615aa 100644 --- a/content-test/selectors/selectors.test.js +++ b/content-test/selectors/selectors.test.js @@ -189,6 +189,31 @@ describe("selectors", () => { }); }); }); + describe("selectHistory keep less filtered rows", () => { + const rows = [ + {filter: ""}, + {filter: "a"}, + {filter: "ab"} + ]; + function doSelect(query) { + return selectHistory(Object.assign({}, fakeState, { + Filter: {query}, + History: {rows} + })); + } + it("should keep only unfiltered for empty", () => { + let state = doSelect(""); + assert.lengthOf(state.History.rows, 1); + }); + it("should keep only unfiltered and partially filtered for partial filter", () => { + let state = doSelect("a"); + assert.lengthOf(state.History.rows, 2); + }); + it("should keep all filtered for full filter", () => { + let state = doSelect("ab"); + assert.lengthOf(state.History.rows, 3); + }); + }); describe("selectNewTabSites", () => { let state; beforeEach(() => { diff --git a/test/test-PlacesProvider.js b/test/test-PlacesProvider.js index f5261f5899..69f2f2e190 100644 --- a/test/test-PlacesProvider.js +++ b/test/test-PlacesProvider.js @@ -244,11 +244,25 @@ exports.test_Links_getRecentLinks = function*(assert) { assert.equal(links[0].url, "https://mozilla3.com/2", "Expected 1-st link"); assert.equal(links[1].url, "https://mozilla4.com/3", "Expected 2-nd link"); - // test beforeDate functionality + // test afterDate functionality links = yield provider.getRecentLinks({afterDate: theDate}); assert.equal(links.length, 2, "should only see two links inserted after the date"); assert.equal(links[0].url, "https://mozilla2.com/1", "Expected 1-st link"); assert.equal(links[1].url, "https://mozilla1.com/0", "Expected 2-nd link"); + + // test filter functionality + links = yield provider.getRecentLinks({filter: ""}); + assert.equal(links.length, 4, "should match all links"); + assert.equal(links[0].filter, "", "should include filter"); + links = yield provider.getRecentLinks({filter: "a"}); + assert.equal(links.length, 4, "should match all links"); + assert.equal(links[0].filter, "a", "should include filter"); + links = yield provider.getRecentLinks({filter: "a1"}); + assert.equal(links.length, 1, "should match just mozilla1"); + assert.equal(links[0].filter, "a1", "should include filter"); + links = yield provider.getRecentLinks({filter: "a 1"}); + assert.equal(links.length, 2, "should match mozilla1 and /1"); + assert.equal(links[0].filter, "a 1", "should include filter"); }; exports.test_Links_getFrecentLinks = function*(assert) {